Skip to content

Commit d71a6e8

Browse files
author
Matthias
committed
feat(slack) : Add file upload functionality
1 parent 17d36da commit d71a6e8

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

plugins/modules/slack.py

+220
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/python
22
# -*- coding: utf-8 -*-
33

4+
# Copyright (c) 2024, Matthias Colin <[email protected]>
45
# Copyright (c) 2020, Lee Goolsbee <[email protected]>
56
# Copyright (c) 2020, Michal Middleton <[email protected]>
67
# Copyright (c) 2017, Steve Pletcher <[email protected]>
@@ -143,6 +144,33 @@
143144
- 'never'
144145
- 'auto'
145146
version_added: 6.1.0
147+
upload_file:
148+
type: dict
149+
description:
150+
- Specify details to upload a file to Slack. The file can include metadata such as an initial comment, alt text, snipped and title.
151+
- See Slack's file upload API for details at U(https://api.slack.com/methods/files.getUploadURLExternal) and U(https://api.slack.com/methods/files.completeUploadExternal).
152+
suboptions:
153+
path:
154+
type: str
155+
description:
156+
- Path to the file on the local system to upload.
157+
required: true
158+
initial_comment:
159+
type: str
160+
description:
161+
- Optional comment to include when uploading the file.
162+
alt_text:
163+
type: str
164+
description:
165+
- Optional alternative text to describe the file.
166+
snippet_type:
167+
type: str
168+
description:
169+
- Optional snippet type for the file.
170+
title:
171+
type: str
172+
description:
173+
- Optional title for the uploaded file.
146174
"""
147175

148176
EXAMPLES = r"""
@@ -254,9 +282,22 @@
254282
channel: "{{ slack_response.channel }}"
255283
msg: Deployment complete!
256284
message_id: "{{ slack_response.ts }}"
285+
286+
- name: Upload a file to Slack
287+
community.general.slack:
288+
token: thetoken/generatedby/slack
289+
channel: 'ansible'
290+
upload_file:
291+
path: /path/to/file.txt
292+
initial_comment: ''
293+
alt_text: ''
294+
snippet_type: ''
295+
title: ''
257296
"""
258297

259298
import re
299+
import json
300+
import os
260301
from ansible.module_utils.basic import AnsibleModule
261302
from ansible.module_utils.six.moves.urllib.parse import urlencode
262303
from ansible.module_utils.urls import fetch_url
@@ -266,6 +307,9 @@
266307
SLACK_POSTMESSAGE_WEBAPI = 'https://slack.com/api/chat.postMessage'
267308
SLACK_UPDATEMESSAGE_WEBAPI = 'https://slack.com/api/chat.update'
268309
SLACK_CONVERSATIONS_HISTORY_WEBAPI = 'https://slack.com/api/conversations.history'
310+
SLACK_GET_UPLOAD_URL_EXTERNAL = 'https://slack.com/api/files.getUploadURLExternal'
311+
SLACK_COMPLETE_UPLOAD_EXTERNAL = 'https://slack.com/api/files.completeUploadExternal'
312+
SLACK_CONVERSATIONS_LIST_WEBAPI = 'https://slack.com/api/conversations.list'
269313

270314
# Escaping quotes and apostrophes to avoid ending string prematurely in ansible call.
271315
# We do not escape other characters used as Slack metacharacters (e.g. &, <, >).
@@ -430,6 +474,159 @@ def do_notify_slack(module, domain, token, payload):
430474
else:
431475
return {'webhook': 'ok'}
432476

477+
def get_channel_id(module, token, channel_name):
478+
url = SLACK_CONVERSATIONS_LIST_WEBAPI
479+
headers = {"Authorization": f"Bearer {token}"}
480+
params = {
481+
"types": "public_channel,private_channel,mpim,im",
482+
"limit": 1000,
483+
"exclude_archived": "true",
484+
}
485+
cursor = None
486+
while True:
487+
if cursor:
488+
params["cursor"] = cursor
489+
query = urlencode(params)
490+
full_url = f"{url}?{query}"
491+
response, info = fetch_url(module, full_url, headers=headers, method="GET")
492+
status = info.get("status")
493+
if status != 200:
494+
error_msg = info.get("msg", "Unknown error")
495+
module.fail_json(
496+
msg=f"Failed to retrieve channels: {error_msg} (HTTP {status})"
497+
)
498+
try:
499+
response_body = response.read().decode("utf-8") if response else ""
500+
data = json.loads(response_body)
501+
except json.JSONDecodeError as e:
502+
module.fail_json(msg=f"JSON decode error: {e}")
503+
if not data.get("ok"):
504+
error = data.get("error", "Unknown error")
505+
module.fail_json(msg=f"Slack API error: {error}")
506+
channels = data.get("channels", [])
507+
for channel in channels:
508+
if channel.get("name") == channel_name:
509+
channel_id = channel.get("id")
510+
return channel_id
511+
cursor = data.get("response_metadata", {}).get("next_cursor")
512+
if not cursor:
513+
break
514+
module.fail_json(msg=f"Channel named '{channel_name}' not found.")
515+
516+
517+
def upload_file_to_slack(module, token, channel, file_upload):
518+
try:
519+
file_path = file_upload["path"]
520+
if not os.path.exists(file_path):
521+
module.fail_json(msg=f"File not found: {file_path}")
522+
# Step 1: Get upload URL
523+
url = SLACK_GET_UPLOAD_URL_EXTERNAL
524+
headers = {
525+
"Authorization": f"Bearer {token}",
526+
"Content-Type": "application/x-www-form-urlencoded",
527+
}
528+
params = urlencode(
529+
{
530+
"filename": file_upload.get("filename", os.path.basename(file_path)),
531+
"length": os.path.getsize(file_path),
532+
**(
533+
{"alt_text": file_upload.get("alt_text")}
534+
if file_upload.get("alt_text")
535+
else {}
536+
),
537+
**(
538+
{"snippet_type": file_upload.get("snippet_type")}
539+
if file_upload.get("snippet_type")
540+
else {}
541+
),
542+
}
543+
)
544+
response, info = fetch_url(
545+
module, f"{url}?{params}", headers=headers, method="GET"
546+
)
547+
if info["status"] != 200:
548+
module.fail_json(
549+
msg=f"Error retrieving upload URL: {info['msg']} (HTTP {info['status']})"
550+
)
551+
try:
552+
upload_url_data = json.load(response)
553+
except json.JSONDecodeError:
554+
module.fail_json(
555+
msg=f"The Slack API response is not valid JSON: {response.read()}"
556+
)
557+
if not upload_url_data.get("ok"):
558+
module.fail_json(
559+
msg=f"Failed to retrieve upload URL: {upload_url_data.get('error')}"
560+
)
561+
upload_url = upload_url_data["upload_url"]
562+
file_id = upload_url_data["file_id"]
563+
# Step 2: Upload file content
564+
try:
565+
with open(file_path, "rb") as file:
566+
file_content = file.read()
567+
response, info = fetch_url(
568+
module,
569+
upload_url,
570+
data=file_content,
571+
headers={"Content-Type": "application/octet-stream"},
572+
method="POST",
573+
)
574+
if info["status"] != 200:
575+
module.fail_json(
576+
msg=f"Error during file upload: {info['msg']} (HTTP {info['status']})"
577+
)
578+
except FileNotFoundError:
579+
module.fail_json(msg=f"The file {file_path} is not found.")
580+
# Step 3: Complete upload
581+
complete_url = SLACK_COMPLETE_UPLOAD_EXTERNAL
582+
files_data = json.dumps(
583+
{
584+
"files": [
585+
{
586+
"id": file_id,
587+
**(
588+
{"title": file_upload.get("title")}
589+
if file_upload.get("title")
590+
else {}
591+
),
592+
}
593+
],
594+
**(
595+
{"initial_comment": file_upload.get("initial_comment")}
596+
if file_upload.get("initial_comment")
597+
else {}
598+
),
599+
**(
600+
{"thread_ts": file_upload.get("thread_ts")}
601+
if file_upload.get("thread_ts")
602+
else {}
603+
),
604+
"channel_id": get_channel_id(module, token, channel),
605+
}
606+
)
607+
headers = {
608+
"Authorization": f"Bearer {token}",
609+
"Content-Type": "application/json",
610+
}
611+
try:
612+
response, info = fetch_url(
613+
module, complete_url, data=files_data, headers=headers, method="POST"
614+
)
615+
if info["status"] != 200:
616+
module.fail_json(
617+
msg=f"Error during upload completion: {info['msg']} (HTTP {info['status']})"
618+
)
619+
upload_url_data = json.load(response)
620+
except json.JSONDecodeError:
621+
module.fail_json(
622+
msg=f"The Slack API response is not valid JSON: {response.read()}"
623+
)
624+
if not upload_url_data.get("ok"):
625+
module.fail_json(msg=f"Failed to complete the upload: {upload_url_data}")
626+
return upload_url_data
627+
except Exception as e:
628+
module.fail_json(msg=f"Error uploading file: {str(e)}")
629+
433630

434631
def main():
435632
module = AnsibleModule(
@@ -450,6 +647,15 @@ def main():
450647
blocks=dict(type='list', elements='dict'),
451648
message_id=dict(type='str'),
452649
prepend_hash=dict(type='str', choices=['always', 'never', 'auto']),
650+
upload_file=dict(type="dict", options=dict(
651+
path=dict(type="str", required=True),
652+
alt_text=dict(type="str"),
653+
snippet_type=dict(type="str"),
654+
initial_comment=dict(type="str"),
655+
thread_ts=dict(type="str"),
656+
title=dict(type="str")
657+
)
658+
),
453659
),
454660
supports_check_mode=True,
455661
)
@@ -469,6 +675,20 @@ def main():
469675
blocks = module.params['blocks']
470676
message_id = module.params['message_id']
471677
prepend_hash = module.params['prepend_hash']
678+
upload_file = module.params["upload_file"]
679+
680+
if upload_file:
681+
try:
682+
upload_response = upload_file_to_slack(
683+
module=module, token=token, channel=channel, file_upload=upload_file
684+
)
685+
module.exit_json(
686+
changed=True,
687+
msg="File uploaded successfully",
688+
upload_response=upload_response,
689+
)
690+
except Exception as e:
691+
module.fail_json(msg=f"Failed to upload file: {str(e)}")
472692

473693
if prepend_hash is None:
474694
module.deprecate(

0 commit comments

Comments
 (0)