1
1
#!/usr/bin/python
2
2
# -*- coding: utf-8 -*-
3
3
4
+ # Copyright (c) 2024, Matthias Colin <[email protected] >
4
5
# Copyright (c) 2020, Lee Goolsbee <[email protected] >
5
6
# Copyright (c) 2020, Michal Middleton <[email protected] >
6
7
# Copyright (c) 2017, Steve Pletcher <[email protected] >
143
144
- 'never'
144
145
- 'auto'
145
146
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.
146
174
"""
147
175
148
176
EXAMPLES = r"""
254
282
channel: "{{ slack_response.channel }}"
255
283
msg: Deployment complete!
256
284
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: ''
257
296
"""
258
297
259
298
import re
299
+ import json
300
+ import os
260
301
from ansible .module_utils .basic import AnsibleModule
261
302
from ansible .module_utils .six .moves .urllib .parse import urlencode
262
303
from ansible .module_utils .urls import fetch_url
266
307
SLACK_POSTMESSAGE_WEBAPI = 'https://slack.com/api/chat.postMessage'
267
308
SLACK_UPDATEMESSAGE_WEBAPI = 'https://slack.com/api/chat.update'
268
309
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'
269
313
270
314
# Escaping quotes and apostrophes to avoid ending string prematurely in ansible call.
271
315
# We do not escape other characters used as Slack metacharacters (e.g. &, <, >).
@@ -430,6 +474,159 @@ def do_notify_slack(module, domain, token, payload):
430
474
else :
431
475
return {'webhook' : 'ok' }
432
476
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
+
433
630
434
631
def main ():
435
632
module = AnsibleModule (
@@ -450,6 +647,15 @@ def main():
450
647
blocks = dict (type = 'list' , elements = 'dict' ),
451
648
message_id = dict (type = 'str' ),
452
649
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
+ ),
453
659
),
454
660
supports_check_mode = True ,
455
661
)
@@ -469,6 +675,20 @@ def main():
469
675
blocks = module .params ['blocks' ]
470
676
message_id = module .params ['message_id' ]
471
677
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 )} " )
472
692
473
693
if prepend_hash is None :
474
694
module .deprecate (
0 commit comments