11import json
22import logging
33from http .client import HTTPResponse
4- from typing import Dict , Union
4+ from typing import Dict , Union , List , Optional
55from urllib .error import HTTPError
66from urllib .request import Request , urlopen
77
88from slack .errors import SlackRequestError
99from .webhook_response import WebhookResponse
1010from ..web import convert_bool_to_0_or_1 , get_user_agent
11+ from ..web .classes .attachments import Attachment
1112from ..web .classes .blocks import Block
1213
1314
@@ -25,27 +26,46 @@ def __init__(
2526 self .default_headers = default_headers
2627
2728 def send (
28- self , body : Dict [str , any ], additional_headers : Dict [str , str ] = {},
29+ self ,
30+ * ,
31+ text : Optional [str ] = None ,
32+ attachments : Optional [List [Union [Dict [str , any ], Attachment ]]] = None ,
33+ blocks : Optional [List [Union [Dict [str , any ], Block ]]] = None ,
34+ response_type : Optional [str ] = None ,
35+ headers : Optional [Dict [str , str ]] = None ,
36+ ) -> WebhookResponse :
37+ """Performs a Slack API request and returns the result.
38+ :param text: the text message (even when having blocks, setting this as well is recommended as it works as fallback)
39+ :param attachments: a collection of attachments
40+ :param blocks: a collection of Block Kit UI components
41+ :param response_type: the type of message (either 'in_channel' or 'ephemeral')
42+ :param headers: request headers to append only for this request
43+ :return: API response
44+ """
45+ return self .send_dict (
46+ body = {
47+ "text" : text ,
48+ "attachments" : attachments ,
49+ "blocks" : blocks ,
50+ "response_type" : response_type ,
51+ },
52+ headers = headers ,
53+ )
54+
55+ def send_dict (
56+ self , body : Dict [str , any ], headers : Optional [Dict [str , str ]] = None
2957 ) -> WebhookResponse :
3058 """Performs a Slack API request and returns the result.
3159 :param body: json data structure (it's still a dict at this point),
3260 if you give this argument, body_params and files will be skipped
33- :param additional_headers : request headers to append only for this request
61+ :param headers : request headers to append only for this request
3462 :return: API response
3563 """
64+ body = {k : v for k , v in body .items () if v is not None }
3665 body = convert_bool_to_0_or_1 (body )
37- self ._parse_blocks (body )
38- if self .logger .level <= logging .DEBUG :
39- self .logger .debug (
40- f"Sending a request - url: { self .url } , "
41- f"body: { body } , "
42- f"additional_headers: { additional_headers } "
43- )
44-
66+ self ._parse_web_class_objects (body )
4567 return self ._perform_http_request (
46- url = self .url ,
47- body = body ,
48- headers = self ._build_request_headers (additional_headers ),
68+ url = self .url , body = body , headers = self ._build_request_headers (headers ),
4969 )
5070
5171 def _perform_http_request (
@@ -57,43 +77,56 @@ def _perform_http_request(
5777 :param headers: complete set of request headers
5878 :return: API response
5979 """
60- body = json .dumps (body ). encode ( "utf-8" )
80+ body = json .dumps (body )
6181 headers ["Content-Type" ] = "application/json;charset=utf-8"
6282
83+ if self .logger .level <= logging .DEBUG :
84+ self .logger .debug (
85+ f"Sending a request - url: { self .url } , body: { body } , headers: { headers } "
86+ )
6387 try :
6488 # for security
6589 if url .lower ().startswith ("http" ):
66- req = Request (method = "POST" , url = url , data = body , headers = headers )
90+ req = Request (
91+ method = "POST" , url = url , data = body .encode ("utf-8" ), headers = headers
92+ )
6793 else :
6894 raise SlackRequestError (f"Invalid URL detected: { url } " )
6995
7096 resp : HTTPResponse = urlopen (req )
71- charset = resp .headers .get_content_charset () or "utf-8"
72- return WebhookResponse (
97+ charset : str = resp .headers .get_content_charset () or "utf-8"
98+ response_body : str = resp .read ().decode (charset )
99+ resp = WebhookResponse (
73100 url = self .url ,
74101 status_code = resp .status ,
75- body = resp . read (). decode ( charset ) ,
102+ body = response_body ,
76103 headers = resp .headers ,
77104 )
105+ self ._debug_log_response (resp )
106+ return resp
107+
78108 except HTTPError as e :
79- charset = e .headers .get_content_charset () or "utf-8"
109+ charset : str = e .headers .get_content_charset () or "utf-8"
110+ response_body : str = resp .read ().decode (charset )
80111 resp = WebhookResponse (
81- url = self .url ,
82- status_code = e .code ,
83- body = e .read ().decode (charset ),
84- headers = e .headers ,
112+ url = self .url , status_code = e .code , body = response_body , headers = e .headers ,
85113 )
86114 if e .code == 429 :
115+ # for backward-compatibility with WebClient (v.2.5.0 or older)
87116 resp .headers ["Retry-After" ] = resp .headers ["retry-after" ]
117+ self ._debug_log_response (resp )
88118 return resp
89119
90120 except Exception as err :
91121 self .logger .error (f"Failed to send a request to Slack API server: { err } " )
92122 raise err
93123
94124 def _build_request_headers (
95- self , additional_headers : Dict [str , str ],
125+ self , additional_headers : Optional [ Dict [str , str ] ],
96126 ) -> Dict [str , str ]:
127+ if additional_headers is None :
128+ return {}
129+
97130 request_headers = {
98131 "User-Agent" : get_user_agent (),
99132 "Content-Type" : "application/json;charset=utf-8" ,
@@ -104,14 +137,30 @@ def _build_request_headers(
104137 return request_headers
105138
106139 @staticmethod
107- def _parse_blocks (body ) -> None :
108- blocks = body .get ("blocks" , None )
140+ def _parse_web_class_objects (body ) -> None :
141+ def to_dict (obj : Union [Dict , Block , Attachment ]):
142+ if isinstance (obj , Block ):
143+ return obj .to_dict ()
144+ if isinstance (obj , Attachment ):
145+ return obj .to_dict ()
146+ return obj
109147
110- def to_dict (b : Union [Dict , Block ]):
111- if isinstance (b , Block ):
112- return b .to_dict ()
113- return b
148+ blocks = body .get ("blocks" , None )
114149
115150 if blocks is not None and isinstance (blocks , list ):
116151 dict_blocks = [to_dict (b ) for b in blocks ]
117152 body .update ({"blocks" : dict_blocks })
153+
154+ attachments = body .get ("attachments" , None )
155+ if attachments is not None and isinstance (attachments , list ):
156+ dict_attachments = [to_dict (a ) for a in attachments ]
157+ body .update ({"attachments" : dict_attachments })
158+
159+ def _debug_log_response (self , resp : WebhookResponse ) -> None :
160+ if self .logger .level <= logging .DEBUG :
161+ self .logger .debug (
162+ "Received the following response - "
163+ f"status: { resp .status_code } , "
164+ f"headers: { (dict (resp .headers ))} , "
165+ f"body: { resp .body } "
166+ )
0 commit comments