1""" 2Module for sending messages to Slack 3 4.. versionadded:: 2015.5.0 5 6:configuration: This module can be used by either passing an api key and version 7 directly or by specifying both in a configuration profile in the salt 8 master/minion config. 9 10 For example: 11 12 .. code-block:: yaml 13 14 slack: 15 api_key: peWcBiMOS9HrZG15peWcBiMOS9HrZG15 16""" 17 18 19import logging 20import urllib.parse 21 22import salt.utils.json 23import salt.utils.slack 24from salt.exceptions import SaltInvocationError 25 26log = logging.getLogger(__name__) 27 28__virtualname__ = "slack" 29 30 31def __virtual__(): 32 """ 33 Return virtual name of the module. 34 35 :return: The virtual name of the module. 36 """ 37 return __virtualname__ 38 39 40def _get_api_key(): 41 api_key = __salt__["config.get"]("slack.api_key") or __salt__["config.get"]( 42 "slack:api_key" 43 ) 44 45 if not api_key: 46 raise SaltInvocationError("No Slack API key found.") 47 48 return api_key 49 50 51def _get_hook_id(): 52 url = __salt__["config.get"]("slack.hook") or __salt__["config.get"]("slack:hook") 53 if not url: 54 raise SaltInvocationError("No Slack WebHook url found") 55 56 return url 57 58 59def list_rooms(api_key=None): 60 """ 61 List all Slack rooms. 62 63 :param api_key: The Slack admin api key. 64 :return: The room list. 65 66 CLI Example: 67 68 .. code-block:: bash 69 70 salt '*' slack.list_rooms 71 72 salt '*' slack.list_rooms api_key=peWcBiMOS9HrZG15peWcBiMOS9HrZG15 73 """ 74 if not api_key: 75 api_key = _get_api_key() 76 return salt.utils.slack.query(function="rooms", api_key=api_key, opts=__opts__) 77 78 79def list_users(api_key=None): 80 """ 81 List all Slack users. 82 83 :param api_key: The Slack admin api key. 84 :return: The user list. 85 86 CLI Example: 87 88 .. code-block:: bash 89 90 salt '*' slack.list_users 91 92 salt '*' slack.list_users api_key=peWcBiMOS9HrZG15peWcBiMOS9HrZG15 93 """ 94 if not api_key: 95 api_key = _get_api_key() 96 return salt.utils.slack.query(function="users", api_key=api_key, opts=__opts__) 97 98 99def find_room(name, api_key=None): 100 """ 101 Find a room by name and return it. 102 103 :param name: The room name. 104 :param api_key: The Slack admin api key. 105 :return: The room object. 106 107 CLI Example: 108 109 .. code-block:: bash 110 111 salt '*' slack.find_room name="random" 112 113 salt '*' slack.find_room name="random" api_key=peWcBiMOS9HrZG15peWcBiMOS9HrZG15 114 """ 115 if not api_key: 116 api_key = _get_api_key() 117 118 # search results don't include the name of the 119 # channel with a hash, if the passed channel name 120 # has a hash we remove it. 121 if name.startswith("#"): 122 name = name[1:] 123 ret = list_rooms(api_key) 124 if ret["res"]: 125 rooms = ret["message"] 126 if rooms: 127 for room in rooms: 128 if room["name"] == name: 129 return room 130 return False 131 132 133def find_user(name, api_key=None): 134 """ 135 Find a user by name and return it. 136 137 :param name: The user name. 138 :param api_key: The Slack admin api key. 139 :return: The user object. 140 141 CLI Example: 142 143 .. code-block:: bash 144 145 salt '*' slack.find_user name="ThomasHatch" 146 147 salt '*' slack.find_user name="ThomasHatch" api_key=peWcBiMOS9HrZG15peWcBiMOS9HrZG15 148 """ 149 if not api_key: 150 api_key = _get_api_key() 151 152 ret = list_users(api_key) 153 if ret["res"]: 154 users = ret["message"] 155 if users: 156 for user in users: 157 if user["name"] == name: 158 return user 159 return False 160 161 162def post_message( 163 channel, 164 message, 165 from_name, 166 api_key=None, 167 icon=None, 168 attachments=None, 169 blocks=None, 170): 171 """ 172 Send a message to a Slack channel. 173 174 .. versionchanged:: 3003 175 Added `attachments` and `blocks` kwargs 176 177 :param channel: The channel name, either will work. 178 :param message: The message to send to the Slack channel. 179 :param from_name: Specify who the message is from. 180 :param api_key: The Slack api key, if not specified in the configuration. 181 :param icon: URL to an image to use as the icon for this message 182 :param attachments: Any attachments to be sent with the message. 183 :param blocks: Any blocks to be sent with the message. 184 :return: Boolean if message was sent successfully. 185 186 CLI Example: 187 188 .. code-block:: bash 189 190 salt '*' slack.post_message channel="Development Room" message="Build is done" from_name="Build Server" 191 192 """ 193 if not api_key: 194 api_key = _get_api_key() 195 196 if not channel: 197 log.error("channel is a required option.") 198 199 # channel must start with a hash or an @ (direct-message channels) 200 if not channel.startswith("#") and not channel.startswith("@"): 201 log.warning( 202 "Channel name must start with a hash or @. " 203 'Prepending a hash and using "#%s" as ' 204 "channel name instead of %s", 205 channel, 206 channel, 207 ) 208 channel = "#{}".format(channel) 209 210 if not from_name: 211 log.error("from_name is a required option.") 212 213 if not message: 214 log.error("message is a required option.") 215 216 if not from_name: 217 log.error("from_name is a required option.") 218 219 parameters = { 220 "channel": channel, 221 "username": from_name, 222 "text": message, 223 "attachments": attachments or [], 224 "blocks": blocks or [], 225 } 226 227 if icon is not None: 228 parameters["icon_url"] = icon 229 230 # Slack wants the body on POST to be urlencoded. 231 result = salt.utils.slack.query( 232 function="message", 233 api_key=api_key, 234 method="POST", 235 header_dict={"Content-Type": "application/x-www-form-urlencoded"}, 236 data=urllib.parse.urlencode(parameters), 237 opts=__opts__, 238 ) 239 240 if result["res"]: 241 return True 242 else: 243 return result 244 245 246def call_hook( 247 message, 248 attachment=None, 249 color="good", 250 short=False, 251 identifier=None, 252 channel=None, 253 username=None, 254 icon_emoji=None, 255): 256 """ 257 Send message to Slack incoming webhook. 258 259 :param message: The topic of message. 260 :param attachment: The message to send to the Slack WebHook. 261 :param color: The color of border of left side 262 :param short: An optional flag indicating whether the value is short 263 enough to be displayed side-by-side with other values. 264 :param identifier: The identifier of WebHook. 265 :param channel: The channel to use instead of the WebHook default. 266 :param username: Username to use instead of WebHook default. 267 :param icon_emoji: Icon to use instead of WebHook default. 268 :return: Boolean if message was sent successfully. 269 270 CLI Example: 271 272 .. code-block:: bash 273 274 salt '*' slack.call_hook message='Hello, from SaltStack' 275 276 """ 277 base_url = "https://hooks.slack.com/services/" 278 if not identifier: 279 identifier = _get_hook_id() 280 281 url = urllib.parse.urljoin(base_url, identifier) 282 283 if not message: 284 log.error("message is required option") 285 286 if attachment: 287 payload = { 288 "attachments": [ 289 { 290 "fallback": message, 291 "color": color, 292 "pretext": message, 293 "fields": [{"value": attachment, "short": short}], 294 } 295 ] 296 } 297 else: 298 payload = { 299 "text": message, 300 } 301 302 if channel: 303 payload["channel"] = channel 304 305 if username: 306 payload["username"] = username 307 308 if icon_emoji: 309 payload["icon_emoji"] = icon_emoji 310 311 data = urllib.parse.urlencode({"payload": salt.utils.json.dumps(payload)}) 312 result = salt.utils.http.query(url, method="POST", data=data, status=True) 313 314 if result["status"] <= 201: 315 return True 316 else: 317 return {"res": False, "message": result.get("body", result["status"])} 318