-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add file upload support using Slack API
- Loading branch information
Matthias
committed
Dec 27, 2024
1 parent
0afd338
commit 3f22d4d
Showing
1 changed file
with
158 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]> | ||
|
@@ -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 = { | ||
|
@@ -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( | ||
|
@@ -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, | ||
) | ||
|
@@ -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): | ||
|