From 05dacba806935b39e37a44728f3c831d60ed8f50 Mon Sep 17 00:00:00 2001 From: Roach Date: Wed, 5 Sep 2018 14:23:35 -0700 Subject: [PATCH 1/6] Remove Python 3.3 from test environments (#346) * Removed Python 3.3 from Travel environments due to dependencies dropping support for it. * Removed 3.3 from Appveyer * Removed 3.3 from setup.py --- .appveyor.yml | 4 ---- .travis.yml | 1 - setup.py | 1 - tox.ini | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index df4770eee..79475caa4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,16 +4,12 @@ environment: matrix: - PYTHON: "C:\\Python27" PYTHON_VERSION: "py27-x86" - - PYTHON: "C:\\Python33" - PYTHON_VERSION: "py33-x86" - PYTHON: "C:\\Python34" PYTHON_VERSION: "py34-x86" - PYTHON: "C:\\Python35" PYTHON_VERSION: "py35-x86" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "py27-x64" - - PYTHON: "C:\\Python33-x64" - PYTHON_VERSION: "py33-x64" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "py34-x64" - PYTHON: "C:\\Python35-x64" diff --git a/.travis.yml b/.travis.yml index 40ed2f0c3..0ce6c9b03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: trusty language: python python: - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" diff --git a/setup.py b/setup.py index b93275584..8b05357cb 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,6 @@ def find_version(*file_paths): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/tox.ini b/tox.ini index 75b6feaf7..488623a07 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ ; for quality analysis, use `tox -e flake8` or just `flake8 slackclient` ; to build the docs, use `tox -e docs` envlist= - py{27,33,34,35,36}, + py{27,34,35,36}, flake8, docs From 0d3677ade4f239b8e039e37adeeb63f99b1bb6b8 Mon Sep 17 00:00:00 2001 From: Roach Date: Wed, 12 Sep 2018 02:16:19 +0200 Subject: [PATCH 2/6] Add automatic access token rotation for workspace apps (#347) --- .gitignore | 3 + .vscode/settings.json | 6 +- README.rst | 109 +++++++---- docs-src/auth.rst | 20 ++- docs-src/metadata.rst | 2 +- docs-src/real_time_messaging.rst | 6 +- docs/_static/doctools.js | 4 +- docs/_static/documentation_options.js | 2 +- docs/about.html | 6 +- docs/auth.html | 26 +-- docs/basic_usage.html | 6 +- docs/changelog.html | 6 +- docs/conversations.html | 6 +- docs/faq.html | 6 +- docs/genindex.html | 6 +- docs/index.html | 6 +- docs/metadata.html | 6 +- docs/objects.inv | Bin 509 -> 517 bytes docs/real_time_messaging.html | 20 +-- docs/search.html | 6 +- docs/searchindex.js | 2 +- slackclient/client.py | 187 ++++++++++++++----- slackclient/exceptions.py | 6 + slackclient/server.py | 93 +++++----- slackclient/slackrequest.py | 73 ++++---- tests/conftest.py | 4 +- tests/test_channel.py | 2 + tests/test_server.py | 20 +-- tests/test_slackclient.py | 250 +++++++++++++++++++++----- tests/test_slackrequest.py | 5 +- 30 files changed, 625 insertions(+), 269 deletions(-) diff --git a/.gitignore b/.gitignore index 3cdc89899..7290c13d3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ cov_* # due to using tox and pytest .tox .cache +.pytest_cache/ +.python-version +pip \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 42212ddd1..94cddd18f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ // Place your settings in this file to overwrite default and user settings. { "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true + "python.linting.flake8Enabled": true, + "python.venvPath": "./venv", + "python.pythonPath": "${workspaceFolder}/venv/bin/python", + "python.formatting.provider": "black", + "editor.formatOnSave": true, } \ No newline at end of file diff --git a/README.rst b/README.rst index 4ffaa9167..3c7265104 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,15 @@ 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. +Documentation +*************** + +For comprehensive method information and usage examples, see the `full documentation `_. + +If you're building a project to receive content and events from Slack, check out the `Python Slack Events API Adapter `_ library. + +You may also review our `Development Roadmap `_ in the project wiki. + Requirements and Installation ****************************** @@ -43,16 +52,8 @@ by pulling down the source code directly into your project: git clone https://github.com/slackapi/python-slackclient.git pip install -r requirements.txt -Documentation --------------- - -For comprehensive method information and usage examples, see the `full documentation `_. - - -You may also review our `Development Roadmap `_ in the project wiki. - Getting Help -------------- +************* If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue: @@ -68,7 +69,6 @@ This package is a modular wrapper designed to make Slack `Web API `_. -See `Tokens & Authentication `_ for API token handling best practices. Sending a message ******************** @@ -79,6 +79,7 @@ To send a message to a channel, use the channel's ID. For IMs, use the user's ID .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -99,6 +100,7 @@ as sending a regular message, but with an additional ``user`` parameter. .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -127,6 +129,7 @@ appear directly in the channel, instead relegated to a kind of forked timeline d .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -145,6 +148,7 @@ set the ``reply_broadcast`` boolean parameter to ``True``. .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -174,6 +178,7 @@ Sometimes you need to delete things. .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -196,6 +201,7 @@ This method adds a reaction (emoji) to an item (``file``, ``file comment``, ``ch .. code-block:: python + import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] @@ -230,22 +236,12 @@ At some point, you'll want to find out what channels are available to your app. .. code-block:: python - from slackclient import SlackClient - - slack_token = os.environ["SLACK_API_TOKEN"] - sc = SlackClient(slack_token) - sc.api_call("channels.list") Archived channels are included by default. You can exclude them by passing ``exclude_archived=1`` to your request. .. code-block:: python - from slackclient import SlackClient - - slack_token = os.environ["SLACK_API_TOKEN"] - sc = SlackClient(slack_token) - sc.api_call( "channels.list", exclude_archived=1 @@ -259,11 +255,6 @@ Once you have the ID for a specific channel, you can fetch information about tha .. code-block:: python - from slackclient import SlackClient - - slack_token = os.environ["SLACK_API_TOKEN"] - sc = SlackClient(slack_token) - sc.api_call( "channels.info", channel="C0XXXXXXX" @@ -277,11 +268,6 @@ Channels are the social hub of most Slack teams. Here's how you hop into one: .. code-block:: python - from slackclient import SlackClient - - slack_token = os.environ["SLACK_API_TOKEN"] - sc = SlackClient(slack_token) - sc.api_call( "channels.join", channel="C0XXXXXXY" @@ -299,11 +285,6 @@ joined one by accident. This is how you leave a channel. .. code-block:: python - from slackclient import SlackClient - - slack_token = os.environ["SLACK_API_TOKEN"] - sc = SlackClient(slack_token) - sc.api_call( "channels.leave", channel="C0XXXXXXX" @@ -311,6 +292,64 @@ joined one by accident. This is how you leave a channel. See `channels.leave `_ for more info. + +Tokens and Authentication +************************** + +The simplest way to create an instance of the client, as shown in the samples above, is to use a bot (xoxb) access token: + +.. code-block:: python + + # Get the access token from environmental variable + slack_token = os.environ["SLACK_API_TOKEN"] + sc = SlackClient(slack_token) + + +The SlackClient library allows you to use a variety of Slack authentication tokens. + +To take advantage of automatic token refresh, you'll need to instantiate the client a little differently than when using +a bot access token. With a bot token, you have the access (xoxb) token when you create the client, when using refresh tokens, +you won't know the access token when the client is created. + +Upon the first request, the SlackClient will request a new access (xoxa) token on behalf of your application, using your app's +refresh token, client ID, and client secret. + +.. code-block:: python + + # Get the access token from environmental variable + slack_refresh_token = os.environ["SLACK_REFRESH_TOKEN"] + slack_client_id = os.environ["SLACK_CLIENT_ID"] + slack_client_secret = os.environ["SLACK_CLIENT_SECRET"] + + +Since your app's access tokens will be expiring and refreshed, the client requires a callback method to be passed in on creation of the client. +Once Slack returns an access token for your app, the SlackClient will call your provided callback to update the access token in your datastore. + +.. code-block:: python + + # This is where you'll add your data store update logic + def token_update_callback(update_data): + print("Enterprise ID: {}".format(update_data["enterprise_id"])) + print("Workspace ID: {}".format(update_data["team_id"])) + print("Access Token: {}".format(update_data["access_token"])) + print("Access Token expires in (ms): {}".format(update_data["expires_in"])) + + # When creating an instance of the client, pass the client details and token update callback + sc = SlackClient( + refresh_token=slack_refresh_token, + client_id=slack_client_id, + client_secret=slack_client_secret, + token_update_callback=token_update_callback + ) + + +Slack will send your callback function the **app's access token**, **token expiration TTL**, **team ID**, and **enterprise ID** (for enterprise workspaces) + + +See `Tokens & Authentication `_ for API token handling best practices. + + + Additional Information ******************************************************************************************** For comprehensive method information and usage examples, see the `full documentation`_. diff --git a/docs-src/auth.rst b/docs-src/auth.rst index 0ef0cb382..9a587c3f2 100644 --- a/docs-src/auth.rst +++ b/docs-src/auth.rst @@ -5,7 +5,9 @@ Tokens & Authentication Handling tokens and other sensitive data ---------------------------------------- -Slack tokens are the keys to your—or your customers’—teams. Keep them secret. Keep them safe. One way to do that is to never explicitly hardcode them. +⚠️ **Slack tokens are the keys to your—or your customers’—data.Keep them secret. Keep them safe.** + +One way to do that is to never explicitly hardcode them. Try to avoid this when possible: @@ -13,13 +15,15 @@ Try to avoid this when possible: token = 'xoxb-abc-1232' -If you commit this code to GitHub, the world gains access to this token’s team. Rather, we recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as: +⚠️ **Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.** + +We recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as: .. code-block:: python SLACK_BOT_TOKEN="xoxb-abc-1232" python myapp.py -Then in your code retrieve the key with: +Then retrieve the key with: .. code-block:: python @@ -34,13 +38,15 @@ You can use the same technique for other kinds of sensitive data that ne’er-do For additional information, please see our `Safely Storing Credentials `_ page. -Test Tokens +Single-Workspace Apps ----------------------- -During development (prior to implementing OAuth) you can use a test token provided by the `Test Token Generator `_. These tokens provide access to your private data and that of your team. +If you're building an application for a single Slack workspace, there's no need to build out the entire OAuth flow. -**Tester tokens are not intended to replace OAuth 2.0 tokens.** Once your app is ready for users, replace this token with a proper OAuth token implementation. +Once you've setup your features, click on the **Install App to Team** button found 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 changes to take effect. -**Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.** +For additional information, see the `Installing Apps `_ of our `Building Slack apps `_ page. The OAuth flow ------------------------- diff --git a/docs-src/metadata.rst b/docs-src/metadata.rst index 6bf95416b..75d613470 100644 --- a/docs-src/metadata.rst +++ b/docs-src/metadata.rst @@ -14,6 +14,6 @@ .. _Contributing: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md .. _contributing guidelines: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md .. _Contributor License Agreement: https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform -.. _Real Time Messaging API: https://api.slack.com/rtm +.. _Real Time Messaging (RTM) API: https://api.slack.com/rtm .. _Web API: https://api.slack.com/web diff --git a/docs-src/real_time_messaging.rst b/docs-src/real_time_messaging.rst index 28af0a455..e651ea687 100644 --- a/docs-src/real_time_messaging.rst +++ b/docs-src/real_time_messaging.rst @@ -1,9 +1,9 @@ .. _real-time-messaging: ============================================== -Real Time Messaging +Real Time Messaging (RTM) ============================================== -The `Real Time Messaging API`_ is a WebSocket-based API that allows you to +The `Real Time Messaging (RTM) API`_ is a WebSocket-based API that allows you to receive events from Slack in real time and send messages as users. If you prefer events to be pushed to you instead, we recommend using the @@ -13,7 +13,7 @@ in `the Events API `_. See :ref:`Tokens & Authentication ` for API token handling best practices. -Connecting to the Real Time Messaging API +Connecting to the RTM API ------------------------------------------ :: diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index 0c15c0099..d8928926b 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -70,7 +70,9 @@ jQuery.fn.highlightText = function(text, className) { if (node.nodeType === 3) { var val = node.nodeValue; var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { var span; var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); if (isInSVG) { diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 8f03b443f..2f1a9d7b4 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ var DOCUMENTATION_OPTIONS = { - URL_ROOT: '', + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), VERSION: '1.0.1', LANGUAGE: 'None', COLLAPSE_INDEX: false, diff --git a/docs/about.html b/docs/about.html index edd95e65b..cc342e2a1 100644 --- a/docs/about.html +++ b/docs/about.html @@ -47,7 +47,7 @@
  • Slack Developer Kit for Python
  • Tokens & Authentication
  • @@ -75,8 +75,8 @@
  • Get conversation members
  • -
  • Real Time Messaging
      -
    • Connecting to the Real Time Messaging API
    • +
    • Real Time Messaging (RTM)
    • -
    • Real Time Messaging
        -
      • Connecting to the Real Time Messaging API
      • +
      • Real Time Messaging (RTM)
          +
        • Connecting to the RTM API
        • rtm.start vs rtm.connect
        • RTM Events
        • Sending messages via the RTM API
        • @@ -144,16 +144,18 @@

          Tokens & Authentication

          Handling tokens and other sensitive data

          -

          Slack tokens are the keys to your—or your customers’—teams. Keep them secret. Keep them safe. One way to do that is to never explicitly hardcode them.

          +

          ⚠️ Slack tokens are the keys to your—or your customers’—data.Keep them secret. Keep them safe.

          +

          One way to do that is to never explicitly hardcode them.

          Try to avoid this when possible:

          token = 'xoxb-abc-1232'
           
          -

          If you commit this code to GitHub, the world gains access to this token’s team. Rather, we recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as:

          +

          ⚠️ Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.

          +

          We recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as:

          SLACK_BOT_TOKEN="xoxb-abc-1232" python myapp.py
           
          -

          Then in your code retrieve the key with:

          +

          Then retrieve the key with:

          import os
           SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
           
          @@ -166,11 +168,13 @@

          Tokens & AuthenticationSafely Storing Credentials page.

          -
          -

          Test Tokens

          -

          During development (prior to implementing OAuth) you can use a test token provided by the Test Token Generator. These tokens provide access to your private data and that of your team.

          -

          Tester tokens are not intended to replace OAuth 2.0 tokens. Once your app is ready for users, replace this token with a proper OAuth token implementation.

          -

          Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.

          +
          +

          Single-Workspace Apps

          +

          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 setup your features, click on the Install App to Team button found 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 changes to take effect.

          +

          For additional information, see the Installing Apps of our Building Slack apps page.

          The OAuth flow

          diff --git a/docs/basic_usage.html b/docs/basic_usage.html index a427e0519..48fb512b3 100644 --- a/docs/basic_usage.html +++ b/docs/basic_usage.html @@ -47,7 +47,7 @@
        • Slack Developer Kit for Python
        • Tokens & Authentication
        • @@ -75,8 +75,8 @@
        • Get conversation members
      • -
      • Real Time Messaging
          -
        • Connecting to the Real Time Messaging API
        • +
        • Real Time Messaging (RTM)
        • -
        • Real Time Messaging
            -
          • Connecting to the Real Time Messaging API
          • +
          • Real Time Messaging (RTM)
          • -
          • Real Time Messaging
              -
            • Connecting to the Real Time Messaging API
            • +
            • Real Time Messaging (RTM)
            • -
            • Real Time Messaging
                -
              • Connecting to the Real Time Messaging API
              • +
              • Real Time Messaging (RTM)
              • -
              • Real Time Messaging
                  -
                • Connecting to the Real Time Messaging API
                • +
                • Real Time Messaging (RTM)
                • -
                • Real Time Messaging
                    -
                  • Connecting to the Real Time Messaging API
                  • +
                  • Real Time Messaging (RTM)
                  • -
                  • Real Time Messaging
                      -
                    • Connecting to the Real Time Messaging API
                    • +
                    • Real Time Messaging (RTM)
                        +
                      • Connecting to the RTM API
                      • rtm.start vs rtm.connect
                      • RTM Events
                      • Sending messages via the RTM API
                      • diff --git a/docs/objects.inv b/docs/objects.inv index f998d6a755a3b04a073520b8d7ec5c698d14119e..6fa1db8dfacbbb9413a8ebfffe002ba9ac170741 100644 GIT binary patch delta 383 zcmV-_0f7Gf1BC>TkAHoW!A`?442JJ{3QI!>4n^XM5K;jlCNyb`ajv}9Yonz}*Q8^6 zdy=Is$r#+?-~Q~_PH?3<1KJdKg~}nh1$4qqWoiMbJ2LD{6$Mz^`qZ8oM2`sNrmLyae5~Jwjp9YiVN`LOC)yHx=B0@(`MX~98 z8q*mz{Fp)Dip2RGlSoP&@jL4px8Y5!>Fd9=wC(J4!~s&hp#t7J(npMyB)KeTAJ-pL zk5Ao^+0F~WWtkX1^9UF>GP!`)J`nmqYl5=dX+c|)8@?kb&>;IM++R4T`5`dhz%%Xa zup5iMaRY1B!g%~(D!bpB47wa4J!n3`y;NYh5%j=nRh;RuIV6L_6EjtGg8FV-LXj9= zQ&N+*&%^Z-;VvL^0ofQZ3}9hhu(U3ijqKH8IlE?Ops<6#a{iN16SjF3U(IX%9KC<; d9{3ln+`~y|k98yHWQC`H!!{EBj&B9vO$k#;xo-df delta 375 zcmV--0f_#E1pNb$kAHPj%TB{E5WM>n0X>NZ)A1_uLBVVqBB9e?Q~$ADXiENaJ0yN6AuMPIX@+qTX?3u8+PlY zZ^FVxcd$I#+Hmc+A&WjntRArt;9hC4!b*Bzjph?Qw#RIA1jR!0UeeHQM<_EZ8p;|n z?mT=yiQE&`p0GX>#sPdd7p$BM7Nd(z(Bs - Real Time Messaging — Slack Developer Kit for Python + Real Time Messaging (RTM) — Slack Developer Kit for Python @@ -47,7 +47,7 @@
                      • Slack Developer Kit for Python
                      • Tokens & Authentication
                      • @@ -75,8 +75,8 @@
                      • Get conversation members
                    • -
                    • Real Time Messaging
                        -
                      • Connecting to the Real Time Messaging API
                      • +
                      • Real Time Messaging (RTM)
                      • -
                      • Real Time Messaging
                          -
                        • Connecting to the Real Time Messaging API
                        • +
                        • Real Time Messaging (RTM)
                            +
                          • Connecting to the RTM API
                          • rtm.start vs rtm.connect
                          • RTM Events
                          • Sending messages via the RTM API
                          • diff --git a/docs/searchindex.js b/docs/searchindex.js index 52bb23440..e8eff7082 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["about","auth","basic_usage","changelog","conversations","faq","index","metadata","real_time_messaging"],envversion:52,filenames:["about.rst","auth.rst","basic_usage.rst","changelog.rst","conversations.rst","faq.rst","index.rst","metadata.rst","real_time_messaging.rst"],objects:{},objnames:{},objtypes:{},terms:{"0cb4bcd6e887b428e27e8059b6278b86ee661aaa":3,"1046cc2375a85a22e94573e2aad954ba7287c886":3,"7d01515cebc80918a29100b0e4793790eb83e7b9":3,"boolean":2,"break":[3,5],"case":5,"class":3,"default":[2,3,4,8],"function":3,"import":[1,2,3,4,8],"int":2,"long":2,"new":[2,3,4,5],"public":1,"return":[1,2,3,4,8],"short":2,"super":5,"switch":3,"true":[2,3,4,8],"try":1,"while":[2,5,8],Added:3,But:5,For:[1,2],IDs:4,IMs:2,One:[1,2],The:[2,4,6,8],Then:1,There:[2,5],These:1,Use:[4,6],__eq__:3,__name__:1,aadd:3,abc:1,abil:3,abl:1,about:[2,3,5],abov:1,acair:3,accept:1,access:[0,1,3,4,5],access_token:1,accid:[2,4],action:[1,3],actual:2,add:[1,2,3,5],added:[3,5],adding:5,addit:[1,2,4],after:2,agre:1,agreement:0,aim:5,all:[2,4,5],allow:[2,3,6,8],alreadi:[2,4],already_in_channel:2,also:[2,8],although:5,alwai:[5,6],ambiti:5,ani:2,anoth:[2,3],anticip:5,aoberoi:3,api:[0,1,3,5],api_cal:[1,2,3,4],app:[0,1,2,4,5,6,8],appear:2,applic:[1,2],appropri:5,approv:1,archiv:2,arg:[1,3],argument:8,around:5,articl:2,assign:2,assist:6,attach:[2,8],attribut:2,auth:[1,3],auth_cod:1,auth_respons:1,authent:[2,3,8],author:1,authorship:2,auto_reconnect:[3,8],automat:[3,8],avail:[1,2,8],avoid:1,back:8,bad:3,base:[4,8],basic:[5,8],becom:2,been:2,befor:[2,3],begin:[1,4],begin_auth:1,behalf:1,below:2,best:[1,2,6,8],between:2,bite:5,bond:6,bot:[1,2,5,6],bot_access_token:1,both:2,box:2,breath:5,broadcast:[2,8],bug:[3,6],build:[0,2,3,5,6],burst:2,busi:[2,4],button:[1,2],c024be91l:8,c0xxxxxx:[2,4],c0xxxxxxx:[2,4],c0xxxxxxy:2,c3ukjtqac:2,cach:3,call:[1,2,3,4,8],can:[1,2,3,5,6,8],cannot:2,caus:3,cclauss:3,chang:[2,3],changelog:5,channel:[3,8],channnel:3,chat:2,check:2,clean:3,client:[1,3,8],client_id:1,client_secret:1,clone:6,code:[0,1,3,5,6,8],com:[0,1,2,3,4,5,6,7,8],combin:2,command:1,comment:2,commit:[1,3],commonli:[2,5],comparison:3,compat:3,complet:[1,2,5,8],complex:[2,8],concret:5,conduct:0,config:1,configur:3,connect:3,consider:2,contain:[1,2],content:5,context:[1,2],continu:[2,5],contribut:0,contributor:[0,3,5],convers:2,copyright:3,could:[1,5],counter:3,coupl:[3,5],cours:6,coverag:5,creat:3,createchild:3,credenti:1,custom:[1,3,6],d024be91l:8,d45285d2f1025899dcd65e259624ee73771f94bb:3,dai:5,databas:1,deal:2,decod:3,deep:5,def:[1,2],defin:1,delai:2,demonstr:5,depend:[1,4],descend:2,describ:3,descript:8,design:2,detail:8,dev4slack:[],develop:[1,2,3,4,7,8],differ:[1,2],direct:[1,2,8],directli:[2,6],discuss:5,doc:[3,4,5,8],document:[1,2,3],doe:[5,8],doesn:3,doing:6,don:[1,5],done:[1,5],down:6,drop:8,duplic:3,dure:1,easier:2,edit:5,elif:2,els:8,email:3,empti:1,encod:3,encount:4,endpoint:[1,2],ensur:5,environ:[1,2,4,5,8],environment:1,ephemer:2,error:[2,3,8],event:5,exact:4,exampl:[1,2],exce:2,excel:5,except:[2,3],exchang:2,excit:[2,5],exclud:2,exclude_archiv:2,exist:4,expect:[5,8],experi:1,expir:[3,8],explicitli:1,expos:3,f7bb8889580cc34471ba1ddc05afc34d1a5efa23:3,fail:[2,3,8],fals:[2,8],featur:6,feedback:5,feel:5,fellow:6,fetch:2,few:1,file:[3,5],file_com:2,file_cont:2,find:2,finish:[2,4],finish_auth:1,first:[5,8],fix:[2,3,5],flask:1,flexibl:6,folder:5,follow:[5,6],forget:[1,5],fork:2,format:[1,2,3,8],from:[0,1,2,3,4,5,8],full:2,fun:2,futur:5,gain:1,gener:[1,5,6],german:2,get:[1,3,8],git:6,github:[1,3,6],given:5,global:1,good:5,grant:1,great:5,greater:2,group:[2,3,4,8],guidelin:[3,5],had:[2,4],hand:5,handl:[2,3,8],hangout:6,hard:6,hardcod:1,harlowja:3,has:[1,4,8],have:[2,5],header:[2,3],hello:[2,8],help:5,here:[1,2,5,6],hop:2,how:[2,4],howev:2,href:1,http:[1,2,3,6,8],hub:2,ids:1,implement:[1,6],importantli:5,improv:3,includ:[1,2,3,4,5,8],incom:1,incorpor:5,incorrectli:3,increas:5,index:3,indic:2,info:[3,4,8],inform:[1,2,8],initi:[1,4,8],insid:5,instead:[1,2,3,8],integr:[2,3,6],intend:1,interact:2,interfac:4,intern:3,introduc:3,invit:2,is_priv:4,issu:[3,5,6],item:[2,5],its:[3,5],itself:2,jayalan:3,join:[3,4],json:[1,2,3,8],just:[2,4,5,6],kamushaden:3,keep:1,kei:1,kelvintaywl:3,kind:[1,2],kit:[1,2,3,4,7,8],know:[1,5],kt5356:3,larg:[3,8],later:1,let:[0,1,2,5],level:1,leverag:6,licens:[0,3],lighter:8,like:[2,3,4,8],limit:3,linear:2,link:[1,3],list:[3,8],load:3,local:3,log:1,login:3,longer:2,look:5,love:5,mai:[2,3,4],maintain:0,make:[1,2,5],mani:[2,5],mayb:[2,4],mean:2,mechan:5,mention:1,messag:3,method:[1,2,4,8],might:2,mode:8,modular:2,more:[2,4,5,8],most:[2,8],move:3,mpdm:3,mpim:[3,4],msg:8,much:5,must:2,myapp:1,myprivatechannel:4,name:[2,3,4],nearli:2,need:[1,2,5,8],nefari:1,never:1,newest:4,node:1,non:3,note:[2,4,5],now:3,number:2,oauth_scop:1,object:[2,3],off:2,onc:[1,2],one:[1,2,4,5],onli:[2,4,8],open:[2,3,4],opensourc:[0,1,2,3,4,5,6,7,8],option:[2,8],order:1,origin:2,other:[5,8],our:[1,4,5,6],out:2,over:[2,3,8],own:2,packag:[2,3],page:[1,2],param:[1,3],paramet:[2,3,4],parent:[2,3],part:5,parti:6,particularli:5,pass:[1,2,3,4,8],payload:8,peopl:2,pep:3,per:2,perform:1,period:2,permiss:1,persist:1,ping:3,pip:6,place:4,platform:[0,5],pleas:[1,5],png:2,point:[2,3],poop:5,possibl:[1,6],post:[1,2,8],post_instal:1,postephemer:2,postmessag:2,practic:[2,8],pre_instal:1,prefer:[6,8],prevent:3,primari:2,print:[2,8],prior:1,privat:[1,3,8],private_channel:4,problem:8,program:5,progress:4,project:[5,6],proper:1,properli:3,properti:3,propos:5,proudli:0,provid:[1,2,4,5,8],proxi:3,public_channel:4,publish:1,pull:[1,5,6],purpos:2,push:8,pypi:6,python:[1,2,3,4,7,8],queri:1,quickli:[2,6],rate:3,rather:[1,3,5,8],read:[1,2],readi:1,readm:3,rebuild:5,receiv:8,recommend:[1,5,6,8],reconnect:[3,8],redirect:1,refer:2,reflect:2,refus:1,regular:2,releg:2,remov:3,replac:1,repli:[3,8],reply_broadcast:[2,8],report:6,repositori:1,request:[1,2,3,4,5,6,8],requir:[1,2,3],respond:2,respons:[1,2,3],responseparseerror:3,restructuredtext:5,result:[2,3],resum:4,retri:2,retriev:1,review:[1,2],rout:1,rst:[3,5],rtm:[3,5],rtm_connect:[3,8],rtm_read:8,rtm_send_messag:[3,8],run:[5,6],runtim:1,safe:1,sai:2,same:[1,2,4,8],sampart:3,sampl:5,save:[1,2],schlueter:3,scope:[1,2],search:3,second:2,secret:1,section:[1,2,5],see:[1,2,4,8],semver:3,send_messag:3,send_slack_messag:2,sent:2,separ:8,server:[1,3,8],servic:6,set:[1,2,3,4,8],share:[1,4],should:1,show:2,sign:1,simpl:2,simpler:2,simpli:[2,8],sinc:1,slack:[1,2,3,4,7,8],slack_api_token:[2,4,8],slack_bot_scop:1,slack_bot_token:1,slack_client_id:1,slack_client_secret:1,slack_token:[2,4,8],slack_user_token:1,slackapi:[0,1,2,3,4,5,6,7,8],slackclient:[0,1,2,3,4,5,6,7,8],slackclienterror:3,slash:1,sleep:[2,8],slightli:2,social:2,socket:8,some:[2,3],someth:5,sometim:2,sorri:3,sourc:6,special:2,specif:[1,2],specifi:2,sphinx:5,src:5,start:[1,2,5],stasfilin:3,state:2,statu:2,step:5,store:1,str:2,string:[1,3,4],stuck:6,submit:5,subsequ:4,succeed:[1,2],success:1,successfulli:[2,8],support:[3,5,8],sure:[2,5],tada:2,take:5,team:[0,1,3,6,8],techniqu:1,tell:[2,3,8],test:[2,3,5,8],tester:1,text:[2,8],than:[2,3,8],thank:3,thei:[2,3,5],them:[1,2],thi:[1,2,4,5,8],thing:[2,3,4,5,6],think:5,thinking_very_much:2,third:6,those:2,thought:5,thread:[3,8],thread_t:2,through:6,thumbsup:2,tidi:3,time:2,timelin:2,timeout:3,timestamp:2,timfeirg:3,titl:2,togeth:2,token:[2,3,4,8],too:2,tool:0,top:1,total:5,tox:[3,5],tracker:[5,6],travi:3,two:[1,5],txt:6,type:[3,4,8],typo:[2,3],u023becgf:8,u0xxxxxxx:2,u2345678901:4,u3456789012:4,unifi:4,uniqu:2,unit:5,until:2,unus:3,updat:[3,5],upon:1,url:[1,3,8],urllib:3,usag:[1,8],use:[1,2,3,5],used:[1,2,8],user:[1,2,3,4,5,8],using:[1,3,5,6,8],valid:1,variabl:1,varieti:1,veri:2,verif:1,version:3,virtualenv:5,visibl:2,visit:6,vote:2,w1234567890:4,wai:[1,2,6,8],wait:[2,5],want:[1,2,5],web:[0,1,3,5,8],webhook:1,websocket:[3,8],welcom:8,well:[1,5],what:[1,2,4],when:[1,2,3],where:[1,3],whether:[2,6],which:[1,2,3],with_team_st:8,within:[1,5],without:8,won:2,word:2,work:[2,3,4,5,6],workflow:6,workspac:4,world:1,wrapper:[2,5],write:5,written:5,xoxb:1,yield:1,you:[0,1,2,3,4,5,6,8],your:[0,1,2,4,5,6,8],yourself:5},titles:["About","Tokens & Authentication","Basic Usage","Changelog","Conversations API","Frequently Asked Questions","Slack Developer Kit for Python","<no title>","Real Time Messaging"],titleterms:{"public":4,Adding:2,The:1,about:[0,4],api:[2,4,8],ask:5,authent:1,basic:2,bug:5,care:5,changelog:3,channel:[2,4],compil:5,connect:8,content:2,contribut:5,convers:4,creat:[2,4],data:1,delet:2,develop:[0,5,6],direct:4,document:5,emoji:2,even:5,event:8,featur:5,file:2,flow:1,found:5,frequent:5,get:[2,4,6],handl:1,hei:5,help:6,how:5,info:2,inform:4,instal:6,join:2,kit:[0,5,6],leav:[2,4],like:5,limit:2,list:[2,4],member:[2,4],messag:[2,4,8],miss:5,multi:4,oauth:1,omg:5,other:1,person:4,privat:4,python:[0,5,6],question:5,rate:2,reaction:2,real:8,remov:2,repli:2,requir:6,rtm:8,send:[2,8],sensit:1,should:5,slack:[0,5,6],start:8,team:2,test:1,thread:2,time:8,token:1,updat:2,upload:2,usag:2,via:8,web:2,what:5,why:5}}) \ No newline at end of file +Search.setIndex({docnames:["about","auth","basic_usage","changelog","conversations","faq","index","metadata","real_time_messaging"],envversion:55,filenames:["about.rst","auth.rst","basic_usage.rst","changelog.rst","conversations.rst","faq.rst","index.rst","metadata.rst","real_time_messaging.rst"],objects:{},objnames:{},objtypes:{},terms:{"0cb4bcd6e887b428e27e8059b6278b86ee661aaa":3,"1046cc2375a85a22e94573e2aad954ba7287c886":3,"7d01515cebc80918a29100b0e4793790eb83e7b9":3,"boolean":2,"break":[3,5],"case":5,"class":3,"default":[2,3,4,8],"function":3,"import":[1,2,3,4,8],"int":2,"long":2,"new":[1,2,3,4,5],"public":1,"return":[1,2,3,4,8],"short":2,"super":5,"switch":3,"true":[2,3,4,8],"try":1,"while":[2,5,8],Added:3,But:5,For:[1,2],IDs:4,IMs:2,One:[1,2],The:[2,4,6,8],Then:1,There:[2,5],These:[],Use:[4,6],__eq__:3,__name__:1,aadd:3,abc:1,abil:3,abl:1,about:[2,3,5],abov:1,acair:3,accept:1,access:[0,1,3,4,5],access_token:1,accid:[2,4],action:[1,3],actual:2,add:[1,2,3,5],added:[3,5],adding:5,addit:[1,2,4],after:[1,2],agre:1,agreement:0,aim:5,all:[2,4,5],allow:[2,3,6,8],alreadi:[2,4],already_in_channel:2,also:[2,8],although:5,alwai:[5,6],ambiti:5,ani:2,anoth:[2,3],anticip:5,aoberoi:3,api:[0,1,3,5],api_cal:[1,2,3,4],app:[0,2,4,5,6,8],appear:2,applic:[1,2],appropri:5,approv:1,archiv:2,arg:[1,3],argument:8,around:5,articl:2,assign:2,assist:6,attach:[2,8],attribut:2,auth:[1,3],auth_cod:1,auth_respons:1,authent:[2,3,8],author:1,authorship:2,auto_reconnect:[3,8],automat:[3,8],avail:[1,2,8],avoid:1,back:8,bad:3,base:[4,8],basic:[5,8],becom:2,been:[1,2],befor:[2,3],begin:[1,4],begin_auth:1,behalf:1,below:2,best:[1,2,6,8],between:2,bite:5,bond:6,bot:[1,2,5,6],bot_access_token:1,both:2,box:2,breath:5,broadcast:[2,8],bug:[3,6],build:[0,1,2,3,5,6],burst:2,busi:[2,4],button:[1,2],c024be91l:8,c0xxxxxx:[2,4],c0xxxxxxx:[2,4],c0xxxxxxy:2,c3ukjtqac:2,cach:3,call:[1,2,3,4,8],can:[1,2,3,5,6,8],cannot:2,caus:3,cclauss:3,chang:[1,2,3],changelog:5,channel:[3,8],channnel:3,chat:2,check:2,clean:3,click:1,client:[1,3,8],client_id:1,client_secret:1,clone:6,code:[0,1,3,5,6,8],com:[0,1,2,3,4,5,6,7,8],combin:2,command:1,comment:2,commit:3,commonli:[2,5],comparison:3,compat:3,complet:[1,2,5,8],complex:[2,8],concret:5,conduct:0,config:1,configur:3,connect:3,consider:2,contain:[1,2],content:5,context:[1,2],continu:[2,5],contribut:0,contributor:[0,3,5],convers:2,copyright:3,could:[1,5],counter:3,coupl:[3,5],cours:6,coverag:5,creat:3,createchild:3,credenti:1,custom:[1,3,6],d024be91l:8,d45285d2f1025899dcd65e259624ee73771f94bb:3,dai:5,databas:1,deal:2,decod:3,deep:5,def:[1,2],defin:1,delai:2,demonstr:5,depend:[1,4],descend:2,describ:3,descript:8,design:2,detail:8,develop:[1,2,3,4,7,8],differ:[1,2],direct:[1,2,8],directli:[2,6],discuss:5,doc:[3,4,5,8],document:[1,2,3],doe:[5,8],doesn:3,doing:6,don:[1,5],done:[1,5],down:6,drop:8,duplic:3,dure:[],easier:2,edit:5,effect:1,elif:2,els:8,email:3,empti:1,encod:3,encount:4,endpoint:[1,2],ensur:5,entir:1,environ:[1,2,4,5,8],environment:1,ephemer:2,error:[2,3,8],event:5,exact:4,exampl:[1,2],exce:2,excel:5,except:[2,3],exchang:2,excit:[2,5],exclud:2,exclude_archiv:2,exist:4,expect:[5,8],experi:1,expir:[3,8],explicitli:1,expos:3,f7bb8889580cc34471ba1ddc05afc34d1a5efa23:3,fail:[2,3,8],fals:[2,8],featur:[1,6],feedback:5,feel:5,fellow:6,fetch:2,few:1,file:[3,5],file_com:2,file_cont:2,find:2,finish:[2,4],finish_auth:1,first:[5,8],fix:[2,3,5],flask:1,flexibl:6,folder:5,follow:[5,6],forget:[1,5],fork:2,format:[1,2,3,8],found:1,from:[0,1,2,3,4,5,8],full:2,fun:2,futur:5,gain:[],gener:[1,5,6],german:2,get:[1,3,8],git:6,github:[3,6],given:5,global:1,good:5,grant:1,great:5,greater:2,group:[2,3,4,8],guidelin:[3,5],had:[2,4],hand:5,handl:[2,3,8],hangout:6,hard:6,hardcod:1,harlowja:3,has:[1,4,8],have:[2,5],header:[2,3],hello:[2,8],help:5,here:[1,2,5,6],hop:2,how:[2,4],howev:2,href:1,http:[1,2,3,6,8],hub:2,ids:1,implement:[1,6],importantli:5,improv:3,includ:[1,2,3,4,5,8],incom:1,incorpor:5,incorrectli:3,increas:5,index:3,indic:2,info:[3,4,8],inform:[1,2,8],initi:[1,4,8],insid:5,instal:1,instead:[1,2,3,8],integr:[2,3,6],intend:[],interact:2,interfac:4,intern:3,introduc:3,invit:2,is_priv:4,issu:[3,5,6],item:[2,5],its:[3,5],itself:2,jayalan:3,join:[3,4],json:[1,2,3,8],just:[2,4,5,6],kamushaden:3,keep:1,kei:1,kelvintaywl:3,kind:[1,2],kit:[1,2,3,4,7,8],know:[1,5],kt5356:3,larg:[3,8],later:1,let:[0,1,2,5],level:1,leverag:6,licens:[0,3],lighter:8,like:[2,3,4,8],limit:3,linear:2,link:[1,3],list:[3,8],load:3,local:3,log:1,login:3,longer:2,look:5,love:5,mai:[2,3,4],maintain:0,make:[1,2,5],mani:[2,5],mayb:[2,4],mean:2,mechan:5,mention:1,messag:3,method:[1,2,4,8],might:2,mode:8,modular:2,more:[2,4,5,8],most:[2,8],move:3,mpdm:3,mpim:[3,4],msg:8,much:5,must:[1,2],myapp:1,myprivatechannel:4,name:[2,3,4],nearli:2,need:[1,2,5,8],nefari:1,never:1,newest:4,node:1,non:3,note:[2,4,5],now:3,number:2,oauth_scop:1,object:[2,3],off:2,onc:[1,2],one:[1,2,4,5],onli:[2,4,8],open:[2,3,4],opensourc:[0,1,2,3,4,5,6,7,8],option:[2,8],order:1,origin:2,other:[5,8],our:[1,4,5,6],out:[1,2],over:[2,3,8],own:2,packag:[2,3],page:[1,2],param:[1,3],paramet:[2,3,4],parent:[2,3],part:5,parti:6,particularli:5,pass:[1,2,3,4,8],payload:8,peopl:2,pep:3,per:2,perform:1,period:2,permiss:1,persist:1,ping:3,pip:6,place:4,platform:[0,5],pleas:[1,5],png:2,point:[2,3],poop:5,possibl:[1,6],post:[1,2,8],post_instal:1,postephemer:2,postmessag:2,practic:[2,8],pre_instal:1,prefer:[6,8],prevent:3,primari:2,print:[2,8],prior:[],privat:[3,8],private_channel:4,problem:8,program:5,progress:4,project:[5,6],proper:[],properli:3,properti:3,propos:5,proudli:0,provid:[1,2,4,5,8],proxi:3,public_channel:4,publish:1,pull:[1,5,6],purpos:2,push:8,pypi:6,python:[1,2,3,4,7,8],queri:1,quickli:[2,6],rate:3,rather:[3,5,8],read:[1,2],readi:[],readm:3,rebuild:5,receiv:8,recommend:[1,5,6,8],reconnect:[3,8],redirect:1,refer:2,reflect:2,refus:1,regular:2,reinstal:1,releg:2,remov:3,replac:[],repli:[3,8],reply_broadcast:[2,8],report:6,repositori:1,request:[1,2,3,4,5,6,8],requir:[1,2,3],respond:2,respons:[1,2,3],responseparseerror:3,restructuredtext:5,result:[2,3],resum:4,retri:2,retriev:1,review:[1,2],rout:1,rst:[3,5],rtm:[3,5],rtm_connect:[3,8],rtm_read:8,rtm_send_messag:[3,8],run:[5,6],runtim:1,safe:1,sai:2,same:[1,2,4,8],sampart:3,sampl:5,save:[1,2],schlueter:3,scope:[1,2],search:3,second:2,secret:1,section:[1,2,5],see:[1,2,4,8],semver:3,send_messag:3,send_slack_messag:2,sent:2,separ:8,server:[1,3,8],servic:6,set:[1,2,3,4,8],setup:1,share:[1,4],should:1,show:2,sign:1,simpl:2,simpler:2,simpli:[2,8],sinc:1,slack:[1,2,3,4,7,8],slack_api_token:[2,4,8],slack_bot_scop:1,slack_bot_token:1,slack_client_id:1,slack_client_secret:1,slack_token:[2,4,8],slack_user_token:1,slackapi:[0,1,2,3,4,5,6,7,8],slackclient:[0,1,2,3,4,5,6,7,8],slackclienterror:3,slash:1,sleep:[2,8],slightli:2,social:2,socket:8,some:[2,3],someth:5,sometim:2,sorri:3,sourc:6,special:2,specif:[1,2],specifi:2,sphinx:5,src:5,start:[1,2,5],stasfilin:3,state:2,statu:2,step:5,store:1,str:2,string:[1,3,4],stuck:6,submit:5,subsequ:4,succeed:[1,2],success:1,successfulli:[2,8],support:[3,5,8],sure:[2,5],tada:2,take:[1,5],team:[0,1,3,6,8],techniqu:1,tell:[2,3,8],test:[1,2,3,5,8],tester:[],text:[2,8],than:[2,3,8],thank:3,thei:[2,3,5],them:[1,2],thi:[1,2,4,5,8],thing:[2,3,4,5,6],think:5,thinking_very_much:2,third:6,those:2,thought:5,thread:[3,8],thread_t:2,through:6,thumbsup:2,tidi:3,time:2,timelin:2,timeout:3,timestamp:2,timfeirg:3,titl:2,togeth:2,token:[2,3,4,8],too:2,tool:0,top:1,total:5,tox:[3,5],tracker:[5,6],travi:3,two:[1,5],txt:6,type:[3,4,8],typo:[2,3],u023becgf:8,u0xxxxxxx:2,u2345678901:4,u3456789012:4,unifi:4,uniqu:2,unit:5,until:2,unus:3,updat:[3,5],upon:1,url:[1,3,8],urllib:3,usag:[1,8],use:[1,2,3,5],used:[1,2,8],user:[1,2,3,4,5,8],using:[1,3,5,6,8],valid:1,variabl:1,varieti:1,veri:2,verif:1,version:3,virtualenv:5,visibl:2,visit:6,vote:2,w1234567890:4,wai:[1,2,6,8],wait:[2,5],want:[1,2,5],web:[0,1,3,5,8],webhook:1,websocket:[3,8],welcom:8,well:[1,5],what:[1,2,4],when:[1,2,3],where:[1,3],whether:[2,6],which:[1,2,3],with_team_st:8,within:[1,5],without:8,won:2,word:2,work:[2,3,4,5,6],workflow:6,workspac:4,world:[],wrapper:[2,5],write:5,written:5,xoxb:1,yield:1,you:[0,1,2,3,4,5,6,8],your:[0,1,2,4,5,6,8],yourself:5},titles:["About","Tokens & Authentication","Basic Usage","Changelog","Conversations API","Frequently Asked Questions","Slack Developer Kit for Python","<no title>","Real Time Messaging (RTM)"],titleterms:{"public":4,Adding:2,The:1,about:[0,4],api:[2,4,8],app:1,ask:5,authent:1,basic:2,bug:5,care:5,changelog:3,channel:[2,4],compil:5,connect:8,content:2,contribut:5,convers:4,creat:[2,4],data:1,delet:2,develop:[0,5,6],direct:4,document:5,emoji:2,even:5,event:8,featur:5,file:2,flow:1,found:5,frequent:5,get:[2,4,6],handl:1,hei:5,help:6,how:5,info:2,inform:4,instal:6,join:2,kit:[0,5,6],leav:[2,4],like:5,limit:2,list:[2,4],member:[2,4],messag:[2,4,8],miss:5,multi:4,oauth:1,omg:5,other:1,person:4,privat:4,python:[0,5,6],question:5,rate:2,reaction:2,real:8,remov:2,repli:2,requir:6,rtm:8,send:[2,8],sensit:1,should:5,singl:1,slack:[0,5,6],start:8,team:2,test:[],thread:2,time:8,token:1,updat:2,upload:2,usag:2,via:8,web:2,what:5,why:5,workspac:1}}) \ No newline at end of file diff --git a/slackclient/client.py b/slackclient/client.py index ab6735ed4..0e3dfde5e 100644 --- a/slackclient/client.py +++ b/slackclient/client.py @@ -3,15 +3,17 @@ import json import logging +import time from .server import Server -from .exceptions import ParseResponseError +from .exceptions import ParseResponseError, TokenRefreshError + LOG = logging.getLogger(__name__) class SlackClient(object): - ''' + """ The SlackClient makes API Calls to the `Slack Web API `_ as well as managing connections to the `Real-time Messaging API via websocket `_ @@ -19,26 +21,105 @@ class SlackClient(object): is associated with. For more information, check out the `Slack API Docs `_ - - Init: - :Args: - token (str): Your Slack Authentication token. You can find or generate a test token - `here `_ - Note: Be `careful with your token `_ - proxies (dict): Proxies to use when create websocket or api calls, - declare http and websocket proxies using {'http': 'http://127.0.0.1'}, - and https proxy using {'https': 'https://127.0.0.1:443'} - ''' - def __init__(self, token, proxies=None): - + """ + + def __init__( + self, + token=None, + refresh_token=None, + token_update_callback=None, + client_id=None, + client_secret=None, + proxies=None, + **kwargs + ): + """ + Init: + :Args: + token (str): Your Slack Authentication token. You can find or generate a test token + `here `_ + Note: Be `careful with your token `_ + proxies (dict): Proxies to use when create websocket or api calls, + declare http and websocket proxies using {'http': 'http://127.0.0.1'}, + and https proxy using {'https': 'https://127.0.0.1:443'} + refresh_token (str): Your Slack app's refresh token. This token is used to + update your app's OAuth access token + client_id (str): Your app's Client ID + client_secret (srt): Your app's Client Secret (Used for OAuth requests) + refresh_callback (function): Your application's function for updating Slack + OAuth tokens inside your data store + """ + + self.client_id = client_id + self.client_secret = client_secret + self.refresh_token = refresh_token + self.token_update_callback = token_update_callback self.token = token - self.server = Server(self.token, False, proxies) + self.access_token_expires_at = 0 + + if refresh_token: + if callable(token_update_callback): + self.server = Server( + connect=False, + proxies=proxies, + refresh_token=refresh_token, + client_id=client_id, + client_secret=client_secret, + token_update_callback=token_update_callback, + ) + else: + raise TokenRefreshError( + "Token refresh callback function is required when using refresh token." + ) + else: + # Slack app configs + self.server = Server(token=token, connect=False, proxies=proxies) + + def refresh_access_token(self): + """ + Refresh the client's OAUth access tokens + https://api.slack.com/docs/rotating-and-refreshing-credentials + """ + post_data = { + "refresh_token": self.refresh_token, + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + } + response = self.server.api_requester.post_http_request( + self.refresh_token, api_method="oauth.access", post_data=post_data + ) + response_json = json.loads(response.text) + + # If Slack returned an updated access token, update the client, otherwise + # raise TokenRefreshError exception with the error returned from the API + if response_json["ok"]: + # Update the client's access token and expiration timestamp + self.team_id = response_json["team_id"] + # TODO: Minimize the numer of places token is stored. + self.token = response_json["access_token"] + self.server.token = response_json["access_token"] + + # Update the token expiration timestamp + current_ts = int(time.time()) + expires_at = int(current_ts + response_json["expires_in"]) + self.access_token_expires_at = expires_at + # Call the developer's token update callback + update_args = { + "enterprise_id": response_json["enterprise_id"], + "team_id": response_json["team_id"], + "access_token": response_json["access_token"], + "expires_in": response_json["expires_in"], + } + self.token_update_callback(update_args) + else: + raise TokenRefreshError("Token refresh failed") def append_user_agent(self, name, version): self.server.append_user_agent(name, version) def rtm_connect(self, with_team_state=True, **kwargs): - ''' + """ Connects to the RTM Websocket :Args: @@ -48,7 +129,12 @@ def rtm_connect(self, with_team_state=True, **kwargs): :Returns: False on exceptions - ''' + """ + + if self.refresh_token: + raise TokenRefreshError( + "Workspace tokens may not be used to connect to the RTM API." + ) try: self.server.rtm_connect(use_rtm_start=with_team_state, **kwargs) @@ -58,7 +144,7 @@ def rtm_connect(self, with_team_state=True, **kwargs): return False def api_call(self, method, timeout=None, **kwargs): - ''' + """ Call the Slack Web API as documented here: https://api.slack.com/web :Args: @@ -70,7 +156,7 @@ def api_call(self, method, timeout=None, **kwargs): Example:: - sc.server.api_call( + sc.api_call( "channels.setPurpose", channel="CABC12345", purpose="Writing some code!" @@ -86,24 +172,46 @@ def api_call(self, method, timeout=None, **kwargs): u'{"ok":false,"error":"channel_not_found"}' See here for more information on responses: https://api.slack.com/web - ''' - response_body = self.server.api_call(method, timeout=timeout, **kwargs) + """ + # Check for missing or expired access token before submitting the request + if method != "oauth.access" and self.refresh_token: + current_ts = int(time.time()) + token_is_expired = current_ts > self.access_token_expires_at + if token_is_expired or self.token is None: + self.refresh_access_token() + + response_body = self.server.api_call( + self.token, request=method, timeout=timeout, **kwargs + ) + + # Attempt to parse the response as JSON try: result = json.loads(response_body) except ValueError as json_decode_error: raise ParseResponseError(response_body, json_decode_error) + response_json = json.loads(response_body) - if "ok" in result and result["ok"]: - if method == 'im.open': + if result.get("ok", False): + if method == "im.open": self.server.attach_channel(kwargs["user"], result["channel"]["id"]) - elif method in ('mpim.open', 'groups.create', 'groups.createchild'): - self.server.parse_channel_data([result['group']]) - elif method in ('channels.create', 'channels.join'): - self.server.parse_channel_data([result['channel']]) + elif method in ("mpim.open", "groups.create", "groups.createchild"): + self.server.parse_channel_data([result["group"]]) + elif method in ("channels.create", "channels.join"): + self.server.parse_channel_data([result["channel"]]) + else: + # if the API request returns an invalid_auth error, refresh the token and try again + if ( + self.refresh_token + and "error" in response_json + and response_json["error"] == "invalid_auth" + ): + self.refresh_access_token() + # If token refresh was successful, retry the original API request + return self.api_call(method, timeout, **kwargs) return result def rtm_read(self): - ''' + """ Reads from the RTM Websocket stream then calls `self.process_changes(item)` for each line in the returned data. @@ -120,14 +228,14 @@ def rtm_read(self): :Raises: SlackNotConnected if self.server is not defined. - ''' + """ # 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'): + if json_data != "": + for d in json_data.split("\n"): data.append(json.loads(d)) for item in data: self.process_changes(item) @@ -136,7 +244,7 @@ def rtm_read(self): raise SlackNotConnected def rtm_send_message(self, channel, message, thread=None, reply_broadcast=None): - ''' + """ Sends a message to a given channel. :Args: @@ -151,33 +259,30 @@ def rtm_send_message(self, channel, message, thread=None, reply_broadcast=None): :Returns: None - ''' + """ # The `channel` argument can be a channel name or an ID. At first its assumed to be a # name and an attempt is made to find the ID in the workspace state cache. # If that lookup fails, the argument is used as the channel ID. found_channel = self.server.channels.find(channel) channel_id = found_channel.id if found_channel else channel return self.server.rtm_send_message( - channel_id, - message, - thread, - reply_broadcast + channel_id, message, thread, reply_broadcast ) def process_changes(self, data): - ''' + """ Internal method which processes RTM events and modifies the local data store accordingly. Stores new channels when joining a group (Multi-party DM), IM (DM) or channel. Stores user data on a team join event. - ''' + """ if "type" in data.keys(): - if data["type"] in ('channel_created', 'group_joined'): + if data["type"] in ("channel_created", "group_joined"): channel = data["channel"] self.server.attach_channel(channel["name"], channel["id"], []) - if data["type"] == 'im_created': + if data["type"] == "im_created": channel = data["channel"] self.server.attach_channel(channel["user"], channel["id"], []) if data["type"] == "team_join": diff --git a/slackclient/exceptions.py b/slackclient/exceptions.py index 09002abe6..d8674567c 100644 --- a/slackclient/exceptions.py +++ b/slackclient/exceptions.py @@ -21,3 +21,9 @@ def __init__(self, response_body, original_exception): ) self.response_body = response_body self.original_exception = original_exception + + +class TokenRefreshError(SlackClientError): + """ + This exception is rasied when a token related error occurs within the client + """ diff --git a/slackclient/server.py b/slackclient/server.py index 92caad732..88b8530df 100644 --- a/slackclient/server.py +++ b/slackclient/server.py @@ -18,13 +18,16 @@ class Server(object): """ The Server object owns the websocket connection and all attached channel information. - - """ - def __init__(self, token, connect=True, proxies=None): - # Slack client configs + + def __init__(self, token=None, connect=True, proxies=None, **kwargs): + # Slack app configs self.token = token + + # api configs self.proxies = proxies + + # HTTP Request handler self.api_requester = SlackRequest(proxies=proxies) # Workspace metadata @@ -100,12 +103,11 @@ def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwarg None """ - # rtm.start returns user and channel info, rtm.connect does not. connect_method = "rtm.start" if use_rtm_start else "rtm.connect" # If the `auto_reconnect` param was passed, set the server's `auto_reconnect` attr - if 'auto_reconnect' in kwargs: + if "auto_reconnect" in kwargs: self.auto_reconnect = kwargs["auto_reconnect"] # If this is an auto reconnect, rate limit reconnect attempts @@ -114,13 +116,17 @@ def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwarg recon_count = self.reconnect_count if recon_count == 5: logging.error("RTM connection failed, reached max reconnects.") - raise SlackConnectionError("RTM connection failed, reached max reconnects.") + raise SlackConnectionError( + "RTM connection failed, reached max reconnects." + ) # Wait to reconnect if the last reconnect was less than 3 minutes ago if (time.time() - self.last_connected_at) < 180: if recon_count > 0: # Back off after the the first attempt backoff_offset_multiplier = random.randint(1, 4) - retry_timeout = (backoff_offset_multiplier * recon_count * recon_count) + retry_timeout = ( + backoff_offset_multiplier * recon_count * recon_count + ) logging.debug("Reconnecting in %d seconds", retry_timeout) time.sleep(retry_timeout) @@ -128,22 +134,28 @@ def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwarg else: self.reconnect_count = 0 - reply = self.api_requester.do(self.token, connect_method, timeout=timeout, post_data=kwargs) + reply = self.api_requester.do( + self.token, connect_method, post_data=kwargs, timeout=timeout + ) if reply.status_code != 200: if self.rtm_connect_retries < 5 and reply.status_code == 429: self.rtm_connect_retries += 1 - retry_after = int(reply.headers.get('retry-after', 120)) - logging.debug("HTTP 429: Rate limited. Retrying in %d seconds", retry_after) + retry_after = int(reply.headers.get("retry-after", 120)) + logging.debug( + "HTTP 429: Rate limited. Retrying in %d seconds", retry_after + ) time.sleep(retry_after) self.rtm_connect(reconnect=reconnect, timeout=timeout) else: - raise SlackConnectionError("RTM connection attempt was rate limited 5 times.") + raise SlackConnectionError( + "RTM connection attempt was rate limited 5 times." + ) else: self.rtm_connect_retries = 0 login_data = reply.json() if login_data["ok"]: - self.ws_url = login_data['url'] + self.ws_url = login_data["url"] self.connect_slack_websocket(self.ws_url) if not reconnect: self.parse_slack_login_data(login_data, use_rtm_start) @@ -164,19 +176,21 @@ def parse_slack_login_data(self, login_data, use_rtm_start): def connect_slack_websocket(self, ws_url): """Uses http proxy if available""" - if self.proxies and 'http' in self.proxies: - parts = parse_url(self.proxies['http']) + if self.proxies and "http" in self.proxies: + parts = parse_url(self.proxies["http"]) proxy_host, proxy_port = parts.host, parts.port auth = parts.auth - proxy_auth = auth and auth.split(':') + proxy_auth = auth and auth.split(":") else: proxy_auth, proxy_port, proxy_host = None, None, None try: - self.websocket = create_connection(ws_url, - http_proxy_host=proxy_host, - http_proxy_port=proxy_port, - http_proxy_auth=proxy_auth) + self.websocket = create_connection( + ws_url, + http_proxy_host=proxy_host, + http_proxy_port=proxy_port, + http_proxy_auth=proxy_auth, + ) self.connected = True self.last_connected_at = time.time() logging.debug("RTM connected") @@ -191,9 +205,7 @@ def parse_channel_data(self, channel_data): channel["name"] = channel["id"] if "members" not in channel: channel["members"] = [] - self.attach_channel(channel["name"], - channel["id"], - channel["members"]) + self.attach_channel(channel["name"], channel["id"], channel["members"]) def parse_user_data(self, user_data): for user in user_data: @@ -203,11 +215,13 @@ def parse_user_data(self, user_data): user["real_name"] = user["name"] if "email" not in user["profile"]: user["profile"]["email"] = "" - self.attach_user(user["name"], - user["id"], - user["real_name"], - user["tz"], - user["profile"]["email"]) + self.attach_user( + user["name"], + user["id"], + user["real_name"], + user["tz"], + user["profile"]["email"], + ) def send_to_websocket(self, data): """ @@ -245,7 +259,7 @@ def rtm_send_message(self, channel, message, thread=None, reply_broadcast=None): if thread is not None: message_json["thread_ts"] = thread if reply_broadcast: - message_json['reply_broadcast'] = True + message_json["reply_broadcast"] = True self.send_to_websocket(message_json) @@ -270,7 +284,7 @@ def websocket_safe_read(self): # # Python 2.7.9+ and Python 3.3+ give this its own exception, # SSLWantReadError - return '' + return "" raise except WebSocketConnectionClosedException as e: logging.debug("RTM disconnected") @@ -278,7 +292,9 @@ def websocket_safe_read(self): if self.auto_reconnect: self.rtm_connect(reconnect=True) else: - raise SlackConnectionError("Unable to send due to closed RTM websocket") + raise SlackConnectionError( + "Unable to send due to closed RTM websocket" + ) return data.rstrip() def attach_user(self, name, user_id, real_name, tz, email): @@ -296,14 +312,10 @@ def join_channel(self, name, timeout=None): Note: this action is not allowed by bots, they must be invited to channels. """ - response = self.api_call( - "channels.join", - channel=name, - timeout=timeout - ) + response = self.api_call("channels.join", channel=name, timeout=timeout) return response - def api_call(self, method, timeout=None, **kwargs): + def api_call(self, token, request="?", timeout=None, **kwargs): """ Call the Slack Web API as documented here: https://api.slack.com/web @@ -334,23 +346,24 @@ def api_call(self, method, timeout=None, **kwargs): See here for more information on responses: https://api.slack.com/web """ - response = self.api_requester.do(self.token, method, kwargs, timeout=timeout) + response = self.api_requester.do(token, request, kwargs, timeout=timeout) response_json = json.loads(response.text) response_json["headers"] = dict(response.headers) return json.dumps(response_json) + # TODO: Move the error types defined below into the .exceptions namespace. This would be a semver # major change because any clients already referencing these types in order to catch them # specifically would need to deal with the symbol names changing. class SlackConnectionError(SlackClientError): - def __init__(self, message='', reply=None): + def __init__(self, message="", reply=None): super(SlackConnectionError, self).__init__(message) self.reply = reply class SlackLoginError(SlackClientError): - def __init__(self, message='', reply=None): + def __init__(self, message="", reply=None): super(SlackLoginError, self).__init__(message) self.reply = reply diff --git a/slackclient/slackrequest.py b/slackclient/slackrequest.py index f48ece24a..3deb29f82 100644 --- a/slackclient/slackrequest.py +++ b/slackclient/slackrequest.py @@ -1,28 +1,29 @@ -import requests import json +import platform +import requests import six import sys -import platform + from .version import __version__ class SlackRequest(object): - def __init__(self, proxies=None): - - # __name__ returns 'slackclient.slackrequest', we only want 'slackclient' - client_name = __name__.split('.')[0] - client_version = __version__ # Version is returned from version.py + def __init__( + self, + proxies=None + ): + # HTTP configs + self.custom_user_agent = None + self.proxies = proxies # Construct the user-agent header with the package info, Python version and OS version. self.default_user_agent = { - "client": "{0}/{1}".format(client_name, client_version), + # __name__ returns all classes, we only want the client + "client": "{0}/{1}".format(__name__.split('.')[0], __version__), "python": "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info), "system": "{0}/{1}".format(platform.system(), platform.release()) } - self.custom_user_agent = None - self.proxies = proxies - def get_user_agent(self): # Check for custom user-agent and append if found if self.custom_user_agent: @@ -44,33 +45,19 @@ def append_user_agent(self, name, version): else: self.custom_user_agent = [[name, version]] - def do(self, token, request="?", post_data=None, domain="slack.com", timeout=None): + def do(self, token=None, request="?", post_data=None, domain="slack.com", timeout=None): """ Perform a POST request to the Slack Web API - Args: token (str): your authentication token request (str): the method to call from the Slack API. For example: 'channels.list' - timeout (float): stop waiting for a response after a given number of seconds post_data (dict): key/value arguments to pass for the request. For example: {'channel': 'CABC12345'} domain (str): if for some reason you want to send your request to something other than slack.com + timeout (float): stop waiting for a response after a given number of seconds """ - - url = 'https://{0}/api/{1}'.format(domain, request) - - # Override token header if `token` is passed in post_data - if post_data is not None and "token" in post_data: - token = post_data['token'] - - # Set user-agent and auth headers - headers = { - 'user-agent': self.get_user_agent(), - 'Authorization': 'Bearer {}'.format(token) - } - - # Pull file out so it isn't JSON encoded like normal fields. + # Pull `file` out so it isn't JSON encoded like normal fields. # Only do this for requests that are UPLOADING files; downloading files # use the 'file' argument to point to a File ID. post_data = post_data or {} @@ -94,12 +81,38 @@ def do(self, token, request="?", post_data=None, domain="slack.com", timeout=Non if isinstance(v, (list, dict)): post_data[k] = json.dumps(v) + return self.post_http_request(token, request, post_data, files, timeout, domain) + + def post_http_request(self, token, api_method, post_data, + files=None, timeout=None, domain="slack.com"): + """ + This method build and submits the Web API HTTP request + + :param token: You app's Slack access token + :param api_method: The API method endpoint to submit the request to + :param post_data: The request payload + :param domain: The URL to submit the API request to + :param files: Any files to be submitted during upload calls + :param timeout: Stop waiting for a response after a given number of seconds + :return: + """ + # Override token header if `token` is passed in post_data + if post_data is not None and "token" in post_data: + token = post_data['token'] + + # Set user-agent and auth headers + headers = { + 'user-agent': self.get_user_agent(), + 'Authorization': 'Bearer {}'.format(token) + } + # Submit the request - return requests.post( - url, + res = requests.post( + 'https://{0}/api/{1}'.format(domain, api_method), headers=headers, data=post_data, files=files, timeout=timeout, proxies=self.proxies ) + return res diff --git a/tests/conftest.py b/tests/conftest.py index 7f2ff0330..9b2571ad7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,12 +22,12 @@ def unverified_post(*args, **kwargs): @pytest.fixture def server(monkeypatch): - my_server = Server('xoxp-1234123412341234-12341234-1234', False) + my_server = Server(token='xoxp-1234123412341234-12341234-1234', connect=False) return my_server @pytest.fixture -def slackclient(server): +def slackclient(): my_slackclient = SlackClient('xoxp-1234123412341234-12341234-1234') return my_slackclient diff --git a/tests/test_channel.py b/tests/test_channel.py index c59adb3b5..88ea5a856 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -14,6 +14,8 @@ def test_channel_eq(channel): assert channel == 'test-channel' assert channel == '#test-channel' assert channel == 'C12345678' + assert 'C12345678' in str(channel) + assert 'C12345678' in "%r" % channel assert (channel == 'foo') is False diff --git a/tests/test_server.py b/tests/test_server.py index 766956ab5..be6e7cb11 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,17 +1,16 @@ import json -import time import pytest -import requests import responses +import time +import urllib3 + from mock import patch + from slackclient.user import User -from slackclient.server import Server, SlackLoginError, SlackConnectionError +from slackclient.server import Server, SlackConnectionError from slackclient.channel import Channel -from requests.packages.urllib3.exceptions import InsecureRequestWarning - - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @pytest.fixture def rtm_start_fixture(): @@ -21,12 +20,11 @@ def rtm_start_fixture(): def test_server(): - server = Server("valid_token", connect=False) + server = Server(token="valid_token", connect=False) assert type(server) == Server # The server eqs to a string, either the token or workspace domain - assert server == "valid_token" - assert server != "invalid_token" + assert server.token == "valid_token" def test_server_connect(rtm_start_fixture): @@ -38,7 +36,7 @@ def test_server_connect(rtm_start_fixture): json=rtm_start_fixture ) - Server("token", connect=True) + Server(token="token", connect=True) for call in rsps.calls: assert call.request.url in [ diff --git a/tests/test_slackclient.py b/tests/test_slackclient.py index 38ac3f7e5..7eaeacad5 100644 --- a/tests/test_slackclient.py +++ b/tests/test_slackclient.py @@ -1,9 +1,8 @@ import json - import pytest from requests.exceptions import ProxyError import responses - +from slackclient.exceptions import TokenRefreshError from slackclient.channel import Channel from slackclient.client import SlackClient from slackclient.server import SlackConnectionError @@ -11,21 +10,21 @@ @pytest.fixture def channel_created_fixture(): - file_channel_created_data = open('tests/data/channel.created.json', 'r').read() + file_channel_created_data = open("tests/data/channel.created.json", "r").read() json_channel_created_data = json.loads(file_channel_created_data) return json_channel_created_data @pytest.fixture def im_created_fixture(): - file_channel_created_data = open('tests/data/im.created.json', 'r').read() + file_channel_created_data = open("tests/data/im.created.json", "r").read() json_channel_created_data = json.loads(file_channel_created_data) return json_channel_created_data def test_proxy(): - proxies = {'http': 'some-bad-proxy', 'https': 'some-bad-proxy'} - client = SlackClient('xoxp-1234123412341234-12341234-1234', proxies=proxies) + proxies = {"http": "some-bad-proxy", "https": "some-bad-proxy"} + client = SlackClient("xoxp-1234123412341234-12341234-1234", proxies=proxies) server = client.server assert server.proxies == proxies @@ -34,48 +33,52 @@ def test_proxy(): server.rtm_connect() with pytest.raises(SlackConnectionError): - server.connect_slack_websocket('wss://mpmulti-xw58.slack-msgs.com/websocket/bad-token') + server.connect_slack_websocket( + "wss://mpmulti-xw58.slack-msgs.com/websocket/bad-token" + ) api_requester = server.api_requester assert api_requester.proxies == proxies with pytest.raises(ProxyError): - api_requester.do('xoxp-1234123412341234-12341234-1234', request='channels.list') + api_requester.do("xoxp-1234123412341234-12341234-1234", request="channels.list") def test_SlackClient(slackclient): assert type(slackclient) == SlackClient -def test_SlackClient_process_changes(slackclient, channel_created_fixture, im_created_fixture): +def test_custom_user_agent(slackclient): + slackclient.append_user_agent("customua", "1.0.0") + assert "customua" in slackclient.server.api_requester.get_user_agent() + + +def test_SlackClient_process_changes( + slackclient, channel_created_fixture, im_created_fixture +): slackclient.process_changes(channel_created_fixture) - assert type(slackclient.server.channels.find('fun')) == Channel + assert type(slackclient.server.channels.find("fun")) == Channel slackclient.process_changes(im_created_fixture) - assert type(slackclient.server.channels.find('U123BL234')) == Channel + assert type(slackclient.server.channels.find("U123BL234")) == Channel def test_api_not_ok(slackclient): # Testing for rate limit retry headers + client = SlackClient("xoxp-1234123412341234-12341234-1234") + with responses.RequestsMock() as rsps: rsps.add( responses.POST, "https://slack.com/api/im.open", status=200, - json={ - "ok": False, - }, - headers={} + json={"ok": False, "error": "invalid_auth"}, + headers={}, ) - slackclient.api_call( - "im.open", - user="UXXXX" - ) + client.api_call("im.open", user="UXXXX") for call in rsps.calls: assert call.response.status_code == 200 - assert call.request.url in [ - "https://slack.com/api/im.open" - ] + assert call.request.url in ["https://slack.com/api/im.open"] def test_im_open(slackclient): @@ -84,23 +87,15 @@ def test_im_open(slackclient): responses.POST, "https://slack.com/api/im.open", status=200, - json={ - "ok": True, - "channel": {"id":"CXXXXXX"} - }, - headers={} + json={"ok": True, "channel": {"id": "CXXXXXX"}}, + headers={}, ) - slackclient.api_call( - "im.open", - user="UXXXX" - ) + slackclient.api_call("im.open", user="UXXXX") for call in rsps.calls: assert call.response.status_code == 200 - assert call.request.url in [ - "https://slack.com/api/im.open" - ] + assert call.request.url in ["https://slack.com/api/im.open"] def test_channel_join(slackclient): @@ -114,20 +109,187 @@ def test_channel_join(slackclient): "channel": { "id": "CXXXX", "name": "test", - "members": ("U0G9QF9C6", "U1QNSQB9U") - } - } + "members": ("U0G9QF9C6", "U1QNSQB9U"), + }, + }, ) - slackclient.api_call( - "channels.join", - channel="CXXXX" - ) + slackclient.api_call("channels.join", channel="CXXXX") for call in rsps.calls: assert call.response.status_code == 200 - assert call.request.url in [ - "https://slack.com/api/channels.join" - ] + assert call.request.url in ["https://slack.com/api/channels.join"] response_json = call.response.json() assert response_json["ok"] is True + + +def test_noncallable_refresh_callback(): + with pytest.raises(TokenRefreshError): + SlackClient( + client_id="12345", + client_secret="12345", + refresh_token="refresh_token", + token_update_callback="THIS IS A STRING, NOT A CALLABLE METHOD", + ) + + +def test_no_RTM_with_workspace_tokens(): + def token_update_callback(update_data): + return update_data + + with pytest.raises(TokenRefreshError): + sc = SlackClient( + client_id="12345", + client_secret="12345", + refresh_token="refresh_token", + token_update_callback=token_update_callback, + ) + + sc.rtm_connect() + + +def test_token_refresh_on_initial_api_request(): + # Client should fetch and append an access token on the first API request + + # When the token is refreshed, the client will call this callback + access_token = "xoxa-2-abcdef" + client_args = {} + + def token_update_callback(update_data): + client_args[update_data["team_id"]] = update_data + + sc = SlackClient( + client_id="12345", + client_secret="12345", + refresh_token="refresh_token", + token_update_callback=token_update_callback, + ) + + # The client starts out with an empty token + assert sc.token is None + + # Mock both the main API request and the token refresh request + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://slack.com/api/auth.test", + status=200, + json={"ok": True}, + ) + + rsps.add( + responses.POST, + "https://slack.com/api/oauth.access", + status=200, + json={ + "ok": True, + "access_token": access_token, + "token_type": "app", + "expires_in": 3600, + "team_id": "T2U81E2FP", + "enterprise_id": "T2U81ELK", + }, + ) + + # Calling the API for the first time will trigger a token refresh + sc.api_call("auth.test") + + # Store the calls in order + calls = {} + for index, call in enumerate(rsps.calls): + calls[index] = {"url": call.request.url} + + # After the initial call, the refresh method will update the client's token, + # then the callback will update client_args + assert sc.token == access_token + assert client_args["T2U81E2FP"]["access_token"] == access_token + + # Verify that the client first tried to call the API, refreshed the token, then retried + assert calls[0]["url"] == "https://slack.com/api/oauth.access" + assert calls[1]["url"] == "https://slack.com/api/auth.test" + + +def test_token_refresh_failed(): + # Client should raise TokenRefreshError is token refresh returns error + def token_update_callback(update_data): + return update_data + + sc = SlackClient( + client_id="12345", + client_secret="12345", + refresh_token="refresh_token", + token_update_callback=token_update_callback, + ) + + with pytest.raises(TokenRefreshError): + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://slack.com/api/channels.list", + status=200, + json={"ok": False, "error": "invalid_auth"}, + ) + + rsps.add( + responses.POST, + "https://slack.com/api/oauth.access", + status=200, + json={"ok": False, "error": "invalid_auth"}, + ) + + sc.api_call("channels.list") + + +def test_token_refresh_on_expired_token(): + # Client should fetch and append an access token on the first API request + + # When the token is refreshed, the client will call this callback + client_args = {} + + def token_update_callback(update_data): + client_args[update_data["team_id"]] = update_data + + sc = SlackClient( + client_id="12345", + client_secret="12345", + refresh_token="refresh_token", + token_update_callback=token_update_callback, + ) + + # Set the token TTL to some time in the past + sc.access_token_expires_at = 0 + + # Mock both the main API request and the token refresh request + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://slack.com/api/auth.test", + status=200, + json={"ok": True}, + ) + + rsps.add( + responses.POST, + "https://slack.com/api/oauth.access", + status=200, + json={ + "ok": True, + "access_token": "xoxa-2-abcdef", + "token_type": "app", + "expires_in": 3600, + "team_id": "T2U81E2FP", + "enterprise_id": "T2U81ELK", + }, + ) + + # Calling the API for the first time will trigger a token refresh + sc.api_call("auth.test") + + # Store the calls in order + calls = {} + for index, call in enumerate(rsps.calls): + calls[index] = {"url": call.request.url} + + # Verify that the client first fetches the token, then submits the request + assert calls[0]["url"] == "https://slack.com/api/oauth.access" + assert calls[1]["url"] == "https://slack.com/api/auth.test" diff --git a/tests/test_slackrequest.py b/tests/test_slackrequest.py index c0ed83f93..0c49915f8 100644 --- a/tests/test_slackrequest.py +++ b/tests/test_slackrequest.py @@ -1,6 +1,5 @@ from slackclient.slackrequest import SlackRequest from slackclient.version import __version__ -import json import os @@ -61,12 +60,12 @@ def test_plural_field(mocker): requests = mocker.patch('slackclient.slackrequest.requests') request = SlackRequest() - request.do('xoxb-123','conversations.open', {'users': ['U123', 'U234', 'U345']}) + request.do('xoxb-123', 'conversations.open', {'users': ['U123', 'U234', 'U345']}) args, kwargs = requests.post.call_args assert kwargs['data'] == {'users': 'U123,U234,U345'} - request.do('xoxb-123','conversations.open', {'users': "U123,U234,U345"}) + request.do('xoxb-123', 'conversations.open', {'users': "U123,U234,U345"}) args2, kwargs2 = requests.post.call_args assert kwargs2['data'] == {'users': 'U123,U234,U345'} From 669cb54fa2522aa417504fc8716db9bcbd823d0e Mon Sep 17 00:00:00 2001 From: Rodney Urquhart <3329665+RodneyU215@users.noreply.github.com> Date: Tue, 11 Sep 2018 18:11:11 -0700 Subject: [PATCH 3/6] 1.3.0 (#350) --- docs-src/changelog.rst | 13 +++++++++++++ slackclient/version.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs-src/changelog.rst b/docs-src/changelog.rst index 709cdc8c6..4845eb27b 100644 --- a/docs-src/changelog.rst +++ b/docs-src/changelog.rst @@ -2,6 +2,19 @@ Changelog ============================================== +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) ------------------- diff --git a/slackclient/version.py b/slackclient/version.py index f097dfc6e..8b856d251 100644 --- a/slackclient/version.py +++ b/slackclient/version.py @@ -1,2 +1,2 @@ # see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers -__version__ = '1.2.1' +__version__ = "1.3.0" From 87630df4665d616b0e294bc888ebb97bdb2300f5 Mon Sep 17 00:00:00 2001 From: Roach Date: Tue, 16 Oct 2018 15:56:24 -0700 Subject: [PATCH 4/6] Update `files.upload` link in docs (#357) * Update basic_usage.html * Updated files.upload link --- docs-src/basic_usage.rst | 2 +- docs/basic_usage.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-src/basic_usage.rst b/docs-src/basic_usage.rst index 00372cce9..bf8de6b80 100644 --- a/docs-src/basic_usage.rst +++ b/docs-src/basic_usage.rst @@ -331,7 +331,7 @@ Uploading files title="Test upload" ) -See `users.list `_ for more info. +See `files.upload `_ for more info. -------- diff --git a/docs/basic_usage.html b/docs/basic_usage.html index 48fb512b3..2140a1a84 100644 --- a/docs/basic_usage.html +++ b/docs/basic_usage.html @@ -413,7 +413,7 @@

                            Uploading files) -

                            See users.list for more info.

                            +

                            See files.upload for more info.