Skip to content

Commit

Permalink
Add file upload support using Slack API
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthias committed Dec 27, 2024
1 parent 0afd338 commit 3f22d4d
Showing 1 changed file with 158 additions and 2 deletions.
160 changes: 158 additions & 2 deletions plugins/modules/slack.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2024, Matthias Colin <[email protected]>
# Copyright (c) 2020, Lee Goolsbee <[email protected]>
# Copyright (c) 2020, Michal Middleton <[email protected]>
# Copyright (c) 2017, Steve Pletcher <[email protected]>
Expand Down Expand Up @@ -251,19 +252,32 @@
channel: "{{ slack_response.channel }}"
msg: Deployment complete!
message_id: "{{ slack_response.ts }}"
- name: Upload a file to Slack
community.general.slack:
token: thetoken/generatedby/slack
channel: 'ansible'
upload_file:
path: /path/to/file.txt
initial_comment: ''
alt_text: ''
snippet_type: ''
"""

import re
import os
import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import fetch_url

OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s'
SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s'
SLACK_POSTMESSAGE_WEBAPI = 'https://slack.com/api/chat.postMessage'
SLACK_UPDATEMESSAGE_WEBAPI = 'https://slack.com/api/chat.update'
SLACK_CONVERSATIONS_HISTORY_WEBAPI = 'https://slack.com/api/conversations.history'

SLACK_GET_UPLOAD_URL_EXTERNAL = 'https://slack.com/api/files.getUploadURLExternal'
SLACK_COMPLETE_UPLOAD_EXTERNAL = 'https://slack.com/api/files.completeUploadExternal'
SLACK_CONVERSATIONS_LIST_WEBAPI = 'https://slack.com/api/conversations.list'
# Escaping quotes and apostrophes to avoid ending string prematurely in ansible call.
# We do not escape other characters used as Slack metacharacters (e.g. &, <, >).
escape_table = {
Expand Down Expand Up @@ -427,6 +441,123 @@ def do_notify_slack(module, domain, token, payload):
else:
return {'webhook': 'ok'}

def get_channel_id(module, token, channel_name):
url = SLACK_CONVERSATIONS_LIST_WEBAPI
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"types": "public_channel,private_channel,mpim,im",
"limit": 1000,
"exclude_archived": "true"
}
cursor = None
while True:
if cursor:
params["cursor"] = cursor
query = urlencode(params)
full_url = f"{url}?{query}"
response, info = fetch_url(module, full_url, headers=headers, method="GET")
status = info.get("status")
if status != 200:
error_msg = info.get("msg", "Unknown error")
module.fail_json(msg=f"Failed to retrieve channels: {error_msg} (HTTP {status})")
try:
response_body = response.read().decode('utf-8') if response else ''
data = json.loads(response_body)
except json.JSONDecodeError as e:
module.fail_json(msg=f"JSON decode error: {e}")
if not data.get("ok"):
error = data.get("error", "Unknown error")
module.fail_json(msg=f"Slack API error: {error}")
channels = data.get("channels", [])
for channel in channels:
if channel.get("name") == channel_name:
channel_id = channel.get("id")
return channel_id
cursor = data.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
module.fail_json(msg=f"Channel named '{channel_name}' not found.")

def upload_file_to_slack(module, token, channel, file_upload):
try:
file_path = file_upload['path']
if not os.path.exists(file_path):
module.fail_json(msg=f"File not found: {file_path}")
# Step 1: Get upload URL
url = SLACK_GET_UPLOAD_URL_EXTERNAL
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/x-www-form-urlencoded"
}
params = urlencode({
'filename': file_upload.get('filename', os.path.basename(file_path)),
'length': os.path.getsize(file_path),
**({"alt_text": file_upload.get('alt_text')} if file_upload.get('alt_text') else {}),
**({"snippet_type": file_upload.get('snippet_type')} if file_upload.get('snippet_type') else {})
})
response, info = fetch_url(
module,
f"{url}?{params}",
headers=headers,
method="GET"
)
if info["status"] != 200:
module.fail_json(msg=f"Error retrieving upload URL: {info['msg']} (HTTP {info['status']})")
try:
upload_url_data = json.load(response)
except json.JSONDecodeError:
module.fail_json(msg=f"The Slack API response is not valid JSON: {response.read()}")
if not upload_url_data.get("ok"):
module.fail_json(msg=f"Failed to retrieve upload URL: {upload_url_data.get('error')}")
upload_url = upload_url_data["upload_url"]
file_id = upload_url_data["file_id"]
# Step 2: Upload file content
try:
with open(file_path, 'rb') as file:
file_content = file.read()
response, info = fetch_url(
module,
upload_url,
data=file_content,
headers={'Content-Type': 'application/octet-stream'},
method="POST"
)
if info['status'] != 200:
module.fail_json(msg=f"Error during file upload: {info['msg']} (HTTP {info['status']})")
except FileNotFoundError:
module.fail_json(msg=f"The file {file_path} is not found.")
# Step 3: Complete upload
complete_url = SLACK_COMPLETE_UPLOAD_EXTERNAL
files_data = json.dumps({
"files": [{"id": file_id, **({"title": file_upload.get('title')} if file_upload.get('title') else {})}],
**({"initial_comment": file_upload.get('initial_comment')} if file_upload.get('initial_comment') else {}),
**({"thread_ts": file_upload.get('thread_ts')} if file_upload.get('thread_ts') else {}),
"channel_id": get_channel_id(module, token, channel)
})
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
try:
response, info = fetch_url(
module,
complete_url,
data=files_data,
headers=headers,
method="POST"
)
if info['status'] != 200:
module.fail_json(msg=f"Error during upload completion: {info['msg']} (HTTP {info['status']})")
upload_url_data = json.load(response)
except json.JSONDecodeError:
module.fail_json(msg=f"The Slack API response is not valid JSON: {response.read()}")
if not upload_url_data.get("ok"):
module.fail_json(msg=f"Failed to complete the upload: {upload_url_data}")
return upload_url_data
except Exception as e:
module.fail_json(msg=f"Error uploading file: {str(e)}")

def main():
module = AnsibleModule(
Expand All @@ -447,6 +578,14 @@ def main():
blocks=dict(type='list', elements='dict'),
message_id=dict(type='str'),
prepend_hash=dict(type='str', default='auto', choices=['always', 'never', 'auto']),
upload_file=dict(type='dict', options=dict(
path=dict(type='str', required=True),
alt_text=dict(type='str'),
snippet_type=dict(type='str'),
initial_comment=dict(type='str'),
thread_ts=dict(type='str'),
title=dict(type='str')
))
),
supports_check_mode=True,
)
Expand All @@ -466,6 +605,23 @@ def main():
blocks = module.params['blocks']
message_id = module.params['message_id']
prepend_hash = module.params['prepend_hash']
upload_file = module.params['upload_file']

if upload_file:
try:
upload_response = upload_file_to_slack(
module=module,
token=token,
channel=channel,
file_upload=upload_file
)
module.exit_json(
changed=True,
msg="File uploaded successfully",
upload_response=upload_response
)
except Exception as e:
module.fail_json(msg=f"Failed to upload file: {str(e)}")

color_choices = ['normal', 'good', 'warning', 'danger']
if color not in color_choices and not is_valid_hex_color(color):
Expand Down

0 comments on commit 3f22d4d

Please sign in to comment.