Source code for flexget.components.notify.notifiers.slack

from loguru import logger
from requests.exceptions import RequestException

from flexget import plugin
from flexget.event import event
from flexget.plugin import PluginWarning
from flexget.utils.requests import Session as RequestSession

requests = RequestSession(max_retries=3)

plugin_name = 'slack'

logger = logger.bind(name=plugin_name)


[docs] class SlackNotifier: """Send a Slack notification. `Sending messages using incoming webhooks <https://api.slack.com/messaging/webhooks>`__ Example:: notify: entries: via: - slack: web_hook_url: "https://hooks.slack.com/services/..." blocks: - section: text: '<{{ tvdb_url }}|{{ series_name }} ({{ series_id }})>' - section: text: '{{ tvdb_ep_overview }}' image: url: "https://api.slack.com/img/blocks/bkb_template_images/plants.png" alt_text: 'plants' fields: - '*Score*' - '*Genres*' - '{{ imdb_score }}' - "{{ imdb_genres | join(', ') | title }}" - '*Rating*' - '*Cast*' - '{{ imdb_mpaa_rating }}' - > {% for key, value in imdb_actors.items() %}{{ value }}{% if not loop.last %}, {% endif %} {% endfor %} - image: url: "{{ tvdb_banner }}" alt_text: "{{ series_name }} Banner" - context: - image: url: 'https://image.freepik.com/free-photo/red-drawing-pin_1156-445.jpg' alt_text: 'red pin' - text: ':round_pushpin:' - text: 'Task: {{ task }}' `Legacy custom integrations incoming webhooks <https://api.slack.com/legacy/custom-integrations/messaging/webhooks>`__ Example (legacy):: notify: entries: via: - slack: web_hook_url: <string> [channel: <string>] (override channel, use "@username" or "#channel") [username: <string>] (override username) [icon_emoji: <string>] (override emoji icon) [icon_url: <string>] (override emoji icon) [attachments: <array>[<object>]] (override attachments) """ schema = { 'definitions': { 'image_block': { 'type': 'object', 'properties': { 'url': {'type': 'string', 'format': 'uri'}, 'alt_text': {'type': 'string'}, }, 'required': ['url', 'alt_text'], 'additionalProperties': False, }, 'image_block_w_title': { 'type': 'object', 'properties': { 'url': {'type': 'string', 'format': 'uri'}, 'title': {'type': 'string'}, 'alt_text': {'type': 'string'}, }, 'required': ['url', 'alt_text'], 'additionalProperties': False, }, }, 'type': 'object', 'properties': { 'web_hook_url': {'type': 'string', 'format': 'uri'}, 'username': {'type': 'string', 'default': 'Flexget'}, 'icon_url': {'type': 'string', 'format': 'uri'}, 'icon_emoji': {'type': 'string'}, 'channel': {'type': 'string'}, 'unfurl_links': {'type': 'boolean'}, 'message': {'type': 'string'}, 'attachments': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'title': {'type': 'string'}, 'author_name': {'type': 'string'}, 'author_link': {'type': 'string'}, 'author_icon': {'type': 'string'}, 'title_link': {'type': 'string'}, 'image_url': {'type': 'string'}, 'thumb_url': {'type': 'string'}, 'footer': {'type': 'string'}, 'footer_icon': {'type': 'string'}, 'ts': {'type': 'number'}, 'fallback': {'type': 'string'}, 'text': {'type': 'string'}, 'pretext': {'type': 'string'}, 'color': {'type': 'string'}, 'fields': { 'type': 'array', 'minItems': 1, 'items': { 'type': 'object', 'properties': { 'title': {'type': 'string'}, 'value': {'type': 'string'}, 'short': {'type': 'boolean'}, }, 'required': ['title'], 'additionalProperties': False, }, }, 'actions': { 'type': 'array', 'minItems': 1, 'items': { 'type': 'object', 'properties': { 'name': {'type': 'string'}, 'text': {'type': 'string'}, 'type': {'type': 'string'}, 'value': {'type': 'string'}, }, 'required': ['name', 'text', 'type', 'value'], 'additionalProperties': False, }, }, 'callback_id': {'type': 'string'}, }, 'required': ['fallback'], 'dependentRequired': {'actions': ['callback_id']}, 'additionalProperties': False, }, }, 'blocks': { 'type': 'array', 'minItems': 1, 'maxItems': 50, 'items': { 'type': 'object', 'properties': { 'section': { 'type': 'object', 'properties': { 'text': {'type': 'string'}, 'image': {'$ref': '#/definitions/image_block'}, 'fields': { 'type': 'array', 'minItems': 1, 'maxItems': 10, 'items': {'type': 'string'}, }, }, 'anyOf': [{'required': ['text']}, {'required': ['fields']}], 'additionalProperties': False, }, 'image': { 'type': 'object', 'properties': { 'text': {'type': 'string'}, 'image': {'$ref': '#/definitions/image_block_w_title'}, }, }, 'context': { 'type': 'array', 'minItems': 1, 'maxItems': 10, 'items': { 'type': 'object', 'properties': { 'text': {'type': 'string'}, 'image': {'$ref': '#/definitions/image_block'}, }, 'anyOf': [ {'required': ['text']}, {'required': ['image']}, ], }, }, 'divider': {'type': 'boolean'}, }, 'additionalProperties': False, }, }, }, 'allOf': [ { 'if': {'required': ['blocks']}, 'then': { 'allOf': [ { 'not': {'required': ['message']}, 'error_not': "Cannot specify 'message' while using version 2 Slack formatting", }, { 'not': {'required': ['attachments']}, 'error_not': "Cannot specify 'attachments' while using version 2 Slack formatting", }, ], }, 'else': { 'anyOf': [{'required': ['message']}, {'required': ['attachments']}], 'allOf': [ { 'not': {'required': ['icon_emoji', 'icon_url']}, 'error_not': "Can only use one of 'icon_emoji' or 'icon_url'", }, { 'not': {'required': ['blocks']}, 'error_not': "Cannot specify 'blocks' while using version 1 Slack formatting", }, ], }, } ], 'required': ['web_hook_url'], 'additionalProperties': False, }
[docs] def notify(self, title, message, config): """Send a Slack notification.""" # version 2 Block Kit formatting if config.get('blocks'): """ Using the Block Kit Builder can help preview how your configuration will be displayed: https://api.slack.com/tools/block-kit-builder?mode=message """ notification = {'blocks': []} for block in config.get('blocks'): if block.get('section'): logger.trace('section block') section = { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': block['section']['text'], }, } if block['section'].get('image', {}).get('url'): section['accessory'] = { 'type': 'image', 'image_url': block['section']['image']['url'], 'alt_text': block['section']['image']['alt_text'], } if block['section'].get('fields'): section['fields'] = [] for field in block['section'].get('fields'): section['fields'].append({'type': 'mrkdwn', 'text': field}) notification['blocks'].append(section) elif block.get('image'): logger.trace('image block') image = { 'type': 'image', 'image_url': block['image']['url'], 'alt_text': block['image']['alt_text'], } if block['image'].get('title'): image['title'] = { 'type': 'plain_text', 'text': block['image']['title'], 'emoji': True, } notification['blocks'].append(image) elif block.get('context'): logger.trace('context block') context = {'type': 'context', 'elements': []} for block_context in block.get('context'): if block_context.get('text'): context['elements'].append({ 'type': 'mrkdwn', 'text': block_context['text'], }) elif block_context.get('image'): context['elements'].append({ 'type': 'image', 'image_url': block_context['image']['url'], 'alt_text': block_context['image']['alt_text'], }) notification['blocks'].append(context) elif block.get('divider'): logger.trace('divider block') notification['blocks'].append({'type': 'divider'}) # version 1 is the Legacy formatting else: # username, channel, icon_emoji, and icon_url are deprecated and ignored by the Slack API, # therefore they've been removed from the posted data notification = { 'text': message, 'username': config.get('username'), 'channel': config.get('channel'), 'attachments': config.get('attachments'), } if config.get('icon_emoji'): notification['icon_emoji'] = f':{config["icon_emoji"].strip(":")}:' if config.get('icon_url'): notification['icon_url'] = config['icon_url'] try: requests.post(config['web_hook_url'], json=notification) except RequestException as e: raise PluginWarning(e.args[0])
[docs] @event('plugin.register') def register_plugin(): plugin.register(SlackNotifier, plugin_name, api_ver=2, interfaces=['notifiers'])