From 36e9911dfc342b75f0a55daac8c4b256d13fe6cd Mon Sep 17 00:00:00 2001 From: Seb Seager Date: Thu, 18 Nov 2021 15:49:15 -0500 Subject: [PATCH] Adopted new slack api auth --- README.md | 26 ++++++--------- bot.py | 10 +++--- exporter.py | 91 +++++++++++++++++++++++++++++++++++++++++------------ slack.yaml | 28 +++++++++++++++++ 4 files changed, 114 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 266b7ad..22c1b24 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,9 @@ There are two ways to use `slack-exporter` (detailed below). Both require a Slac 1. Visit [https://api.slack.com/apps/](https://api.slack.com/apps/) and sign in to your workspace. 2. Click `Create New App`, enter a name (e.g., `Slack Exporter`), and select your workspace. -3. In the left-hand panel, navigate to `OAuth & Permissions`, and scroll to `User Token Scopes` (**not** `Bot Token Scopes`). -4. Select the following permissions: - - `channels:read`, `channels:history` - - `groups:read`, `groups:history` - - `mpim:read`, `mpim:history` - - `im:read`, `im:history` - - `users:read` -5. Select `Install to Workspace` at the top of that page (or `Reinstall to Workspace` if you have done this previously) and accept at the prompt. -6. Copy the `OAuth Access Token` (which will generally start with `xoxp` for user-level permissions) +3. In prior versions of the Slack API, OAuth permissions had to be specified manually. Now, when prompted for an App Manifest, just paste in the contents of the `slack.yaml` file in the root of this repo. +4. Select `Install to Workspace` at the top of that page (or `Reinstall to Workspace` if you have done this previously) and accept at the prompt. +5. Copy the `OAuth Access Token` (which will generally start with `xoxp` for user-level permissions) ## Usage @@ -56,16 +50,14 @@ To use the ngrok method: 2. Run `python bot.py` 3. Run the ngrok binary with `path/to/ngrok http 5000`, where `5000` is the port on which the Flask application (step 2) is running. Copy the forwarding HTTPS address provided. -Return to the Slack app you created in [Authentication with Slack](#authentication-with-slack) and navigate to the `Slash Commands` page in the sidebar. Create the following slash commands (one for each applicable Flask route in `bot.py`): +4. Create the following slash commands will be created (one for each applicable Flask route in `bot.py`): -| Command | Request URL | Arguments | Example Usage | -|-----------------|-------------------------------------------|--------------|----------------------| -| /export-channel | https://`[host_url]`/slack/export-channel | json \| text | /export-channel text | -| /export-replies | https://`[host_url]`/slack/export-replies | json \| text | /export-replies json | + | Command | Request URL | Arguments | Example Usage | + |-----------------|-------------------------------------------|--------------|----------------------| + | /export-channel | https://`[host_url]`/slack/export-channel | json \| text | /export-channel text | + | /export-replies | https://`[host_url]`/slack/export-replies | json \| text | /export-replies json | -where, if using ngrok, `[domain]` would be replaced with something like `https://xxxxxxxxxxxx.ngrok.io`. - -Navigate back to `OAuth & Permissions` and click `(Re)install to Workspace` to add these slash commands to the workspace. + To do this, uncomment the `slash-commands` section in `slack.yaml` and replace `YOUR_HOST_URL_HERE` with something like `https://xxxxxxxxxxxx.ngrok.io` (if using ngrok). Then navigate back to `OAuth & Permissions` and click `(Re)install to Workspace` to add these slash commands to the workspace (ensure the OAuth token in your `.env` file is still correct). ## Author diff --git a/bot.py b/bot.py index 24ae8f9..b9e6d5d 100644 --- a/bot.py +++ b/bot.py @@ -113,7 +113,7 @@ def channel_replies(timestamps, channel_id, response_url): # Flask routes -@app.route("/slack/export-channel", methods=["POST"]) +@app.route("/slack/events/export-channel", methods=["POST"]) def export_channel(): data = request.form @@ -168,7 +168,7 @@ def export_channel(): return Response(), 200 -@app.route("/slack/export-replies", methods=["POST"]) +@app.route("/slack/events/export-replies", methods=["POST"]) def export_replies(): data = request.form @@ -187,7 +187,9 @@ def export_replies(): ch_hist = channel_history(ch_id, response_url) print(ch_hist) ch_replies = channel_replies( - [x["ts"] for x in ch_hist if "reply_count" in x], ch_id, response_url + [x["ts"] for x in ch_hist if "reply_count" in x], + ch_id, + response_url=response_url, ) export_mode = str(command_args).lower() @@ -244,4 +246,4 @@ def download(filename): if __name__ == "__main__": - app.run(debug=False) + app.run(debug=True) diff --git a/exporter.py b/exporter.py index 0153df1..a962561 100644 --- a/exporter.py +++ b/exporter.py @@ -11,22 +11,49 @@ if os.path.isfile(env_file): load_dotenv(env_file) +# write handling + + +def post_response(response_url, text): + requests.post(response_url, json={"text": text}) + + +# use this to say anything +# will print to stdout if no response_url is given +# or post_response to given url if provided +def handle_print(text, response_url=None): + if response_url is None: + print(text) + else: + post_response(response_url, text) + + # pagination handling -def get_at_cursor(url, params, cursor=None): +def get_at_cursor(url, params, cursor=None, response_url=None): if cursor is not None: params["cursor"] = cursor - r = requests.get(url, params=params) - if r.status_code != 200: - print("ERROR: %s %s" % (r.status_code, r.reason)) + # slack api (OAuth 2.0) now requires auth tokens in HTTP Authorization header + # instead of passing it as a query parameter + try: + headers = {"Authorization": "Bearer %s" % os.environ["SLACK_USER_TOKEN"]} + except KeyError: + handle_print("Missing SLACK_USER_TOKEN in environment variables", response_url) sys.exit(1) + + r = requests.get(url, headers=headers, params=params) + + if r.status_code != 200: + handle_print("ERROR: %s %s" % (r.status_code, r.reason), response_url) + sys.exit(1) + d = r.json() try: if d["ok"] is False: - print("I encountered an error: %s" % d) + handle_print("I encountered an error: %s" % d, response_url) sys.exit(1) next_cursor = None @@ -38,16 +65,26 @@ def get_at_cursor(url, params, cursor=None): return next_cursor, d except KeyError as e: - print("Something went wrong: %s." % e) + handle_print("Something went wrong: %s." % e, response_url) return None, [] -def paginated_get(url, params, combine_key=None): +def paginated_get(url, params, combine_key=None, response_url=None): next_cursor = None result = [] while True: - next_cursor, data = get_at_cursor(url, params, cursor=next_cursor) - result.extend(data) if combine_key is None else result.extend(data[combine_key]) + next_cursor, data = get_at_cursor( + url, params, cursor=next_cursor, response_url=response_url + ) + + try: + result.extend(data) if combine_key is None else result.extend( + data[combine_key] + ) + except KeyError: + handle_print("Something went wrong: %s." % e, response_url) + sys.exit(1) + if next_cursor is None: break @@ -57,44 +94,57 @@ def paginated_get(url, params, combine_key=None): # GET requests -def channel_list(team_id=None): +def channel_list(team_id=None, response_url=None): params = { - "token": os.environ["SLACK_USER_TOKEN"], + # "token": os.environ["SLACK_USER_TOKEN"], "team_id": team_id, "types": "public_channel,private_channel,mpim,im", "limit": 200, } return paginated_get( - "https://slack.com/api/conversations.list", params, combine_key="channels" + "https://slack.com/api/conversations.list", + params, + combine_key="channels", + response_url=response_url, ) -def channel_history(channel_id): +def channel_history(channel_id, response_url=None): params = { - "token": os.environ["SLACK_USER_TOKEN"], + # "token": os.environ["SLACK_USER_TOKEN"], "channel": channel_id, "limit": 200, } return paginated_get( - "https://slack.com/api/conversations.history", params, combine_key="messages" + "https://slack.com/api/conversations.history", + params, + combine_key="messages", + response_url=response_url, ) -def user_list(team_id=None): - params = {"token": os.environ["SLACK_USER_TOKEN"], "limit": 200, "team_id": team_id} +def user_list(team_id=None, response_url=None): + params = { + # "token": os.environ["SLACK_USER_TOKEN"], + "limit": 200, + "team_id": team_id, + } return paginated_get( - "https://slack.com/api/users.list", params, combine_key="members" + "https://slack.com/api/users.list", + params, + combine_key="members", + response_url=response_url, ) -def channel_replies(timestamps, channel_id): +def channel_replies(timestamps, channel_id, response_url=None): replies = [] for timestamp in timestamps: params = { - "token": os.environ["SLACK_USER_TOKEN"], + # "token": os.environ["SLACK_USER_TOKEN"], "channel": channel_id, "ts": timestamp, "limit": 200, @@ -104,6 +154,7 @@ def channel_replies(timestamps, channel_id): "https://slack.com/api/conversations.replies", params, combine_key="messages", + response_url=response_url, ) ) diff --git a/slack.yaml b/slack.yaml index 9268590..b6c792d 100644 --- a/slack.yaml +++ b/slack.yaml @@ -1,8 +1,28 @@ _metadata: major_version: 1 + minor_version: 1 display_information: name: Slack Exporter description: Export publicly visible channel data as text or JSON. +features: + app_home: + home_tab_enabled: false + messages_tab_enabled: true + messages_tab_read_only_enabled: false + bot_user: + display_name: Slack Exporter + always_online: true + # slash_commands: + # - command: /export-channel + # description: Export the contents of this channel in either text or JSON format + # usage_hint: json|text + # url: https://YOUR_HOST_URL_HERE/slack/export-channel + # should_escape: false + # - command: /export-replies + # description: Export reply threads in either text or JSON format + # usage_hint: json|text + # url: https://YOUR_HOST_URL_HERE/slack/export-replies + # should_escape: false oauth_config: scopes: user: @@ -15,3 +35,11 @@ oauth_config: - im:read - im:history - users:read + bot: + - commands + - chat:write + - chat:write.public +settings: + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false \ No newline at end of file