mirror of
https://github.com/caronc/apprise.git
synced 2025-03-13 14:28:23 +01:00
Merge ad3ef7d3bc
into 9bf45e415d
This commit is contained in:
commit
3101a1ea77
@ -27,7 +27,13 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
# There are 2 ways to use this plugin...
|
# There are 2 ways to use this plugin...
|
||||||
# Method 1: Via Webhook:
|
# Method 1 : Via Webhook:
|
||||||
|
# Visit https://api.slack.com/apps
|
||||||
|
# - Click on 'Create new App'
|
||||||
|
# - Create one from Scratch
|
||||||
|
# - Provide it an 'App Name' and 'Workspace'
|
||||||
|
|
||||||
|
# Method 1 (legacy) : Via Webhook:
|
||||||
# Visit https://my.slack.com/services/new/incoming-webhook/
|
# Visit https://my.slack.com/services/new/incoming-webhook/
|
||||||
# to create a new incoming webhook for your account. You'll need to
|
# to create a new incoming webhook for your account. You'll need to
|
||||||
# follow the wizard to pre-determine the channel(s) you want your
|
# follow the wizard to pre-determine the channel(s) you want your
|
||||||
@ -38,7 +44,7 @@
|
|||||||
# | | |
|
# | | |
|
||||||
# These are important <--------------^---------^---------------^
|
# These are important <--------------^---------^---------------^
|
||||||
#
|
#
|
||||||
# Method 2: Via a Bot:
|
# Method 2 (legacy) : Via a Bot:
|
||||||
# 1. visit: https://api.slack.com/apps?new_app=1
|
# 1. visit: https://api.slack.com/apps?new_app=1
|
||||||
# 2. Pick an App Name (such as Apprise) and select your workspace. Then
|
# 2. Pick an App Name (such as Apprise) and select your workspace. Then
|
||||||
# press 'Create App'
|
# press 'Create App'
|
||||||
@ -77,13 +83,13 @@ import requests
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
from json import loads
|
from json import loads
|
||||||
from time import time
|
from time import time
|
||||||
|
from datetime import (datetime, timezone)
|
||||||
from .base import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..utils.parse import (
|
from ..utils.parse import (
|
||||||
is_email, parse_bool, parse_list, validate_regex)
|
is_email, parse_bool, parse_list, validate_regex, urlencode)
|
||||||
from ..locale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
@ -96,7 +102,19 @@ CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
|||||||
|
|
||||||
# Channel Regular Expression Parsing
|
# Channel Regular Expression Parsing
|
||||||
CHANNEL_RE = re.compile(
|
CHANNEL_RE = re.compile(
|
||||||
r'^(?P<channel>[+#@]?[A-Z0-9_-]{1,32})(:(?P<thread_ts>[0-9.]+))?$', re.I)
|
r'^(?P<channel>[+#@]?[a-z0-9_-]{1,32})(:(?P<thread_ts>[0-9.]+))?$', re.I)
|
||||||
|
|
||||||
|
# Webhook
|
||||||
|
WEBHOOK_RE = re.compile(
|
||||||
|
r'^([a-z]{4,5}://([^/:]+:)?([^/@]+@)?)?'
|
||||||
|
r'(?P<webhook>[a-z0-9]{9,12}/+[a-z0-9]{9,12}/+'
|
||||||
|
r'[a-z0-9]{20,24})([/?].*|\s*$)', re.I)
|
||||||
|
|
||||||
|
# For detecting Slack API v2 Client IDs
|
||||||
|
CLIENT_ID_RE = re.compile(r'^\d{8,}\.\d{8,}$', re.I)
|
||||||
|
|
||||||
|
# For detecting Slack API v2 Codes
|
||||||
|
CODE_RE = re.compile(r'^[a-z0-9_-]{10,}$', re.I)
|
||||||
|
|
||||||
|
|
||||||
class SlackMode:
|
class SlackMode:
|
||||||
@ -120,6 +138,37 @@ SLACK_MODES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SlackAPIVersion:
|
||||||
|
"""
|
||||||
|
Slack API Version
|
||||||
|
"""
|
||||||
|
# Original - Said to be depricated on March 31st, 2025
|
||||||
|
ONE = '1'
|
||||||
|
|
||||||
|
# New 2024 API Format
|
||||||
|
TWO = '2'
|
||||||
|
|
||||||
|
|
||||||
|
SLACK_API_VERSION_MAP = {
|
||||||
|
# v1
|
||||||
|
"v1": SlackAPIVersion.ONE,
|
||||||
|
"1": SlackAPIVersion.ONE,
|
||||||
|
# v2
|
||||||
|
"v2": SlackAPIVersion.TWO,
|
||||||
|
"2": SlackAPIVersion.TWO,
|
||||||
|
"2024": SlackAPIVersion.TWO,
|
||||||
|
"2025": SlackAPIVersion.TWO,
|
||||||
|
"default": SlackAPIVersion.ONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SLACK_API_VERSIONS = {
|
||||||
|
# Note: This also acts as a reverse lookup mapping
|
||||||
|
SlackAPIVersion.ONE: 'v1',
|
||||||
|
SlackAPIVersion.TWO: 'v2',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifySlack(NotifyBase):
|
class NotifySlack(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Slack Notifications
|
A wrapper for Slack Notifications
|
||||||
@ -164,13 +213,28 @@ class NotifySlack(NotifyBase):
|
|||||||
# becomes the default channel in BOT mode
|
# becomes the default channel in BOT mode
|
||||||
default_notification_channel = '#general'
|
default_notification_channel = '#general'
|
||||||
|
|
||||||
|
# The scopes required to work with Slack
|
||||||
|
slack_v2_oauth_scopes = (
|
||||||
|
# Required for creating a message
|
||||||
|
'chat:write',
|
||||||
|
# Required for attachments
|
||||||
|
'files:write',
|
||||||
|
# Required for looking up a user id when provided ones email
|
||||||
|
'users:read.email'
|
||||||
|
)
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
# Webhook
|
# Webhook (2024+)
|
||||||
'{schema}://{token_a}/{token_b}/{token_c}',
|
'{schema}://{client_id}/{secret}/', # code-aquisition URL
|
||||||
'{schema}://{botname}@{token_a}/{token_b}{token_c}',
|
'{schema}://{client_id}/{secret}/{code}',
|
||||||
'{schema}://{token_a}/{token_b}/{token_c}/{targets}',
|
'{schema}://{client_id}/{secret}/{code}/{targets}',
|
||||||
'{schema}://{botname}@{token_a}/{token_b}/{token_c}/{targets}',
|
|
||||||
|
# Webhook (legacy)
|
||||||
|
'{schema}://{token}',
|
||||||
|
'{schema}://{botname}@{token}',
|
||||||
|
'{schema}://{token}/{targets}',
|
||||||
|
'{schema}://{botname}@{token}/{targets}',
|
||||||
|
|
||||||
# Bot
|
# Bot
|
||||||
'{schema}://{access_token}/',
|
'{schema}://{access_token}/',
|
||||||
@ -179,6 +243,24 @@ class NotifySlack(NotifyBase):
|
|||||||
|
|
||||||
# Define our template tokens
|
# Define our template tokens
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
# Slack API v2 (2024+)
|
||||||
|
'client_id': {
|
||||||
|
'name': _('Client ID'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'secret': {
|
||||||
|
'name': _('Client Secret'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'code': {
|
||||||
|
'name': _('Access Code'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
# Legacy Slack API v1
|
||||||
'botname': {
|
'botname': {
|
||||||
'name': _('Bot Name'),
|
'name': _('Bot Name'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
@ -191,32 +273,15 @@ class NotifySlack(NotifyBase):
|
|||||||
'name': _('OAuth Access Token'),
|
'name': _('OAuth Access Token'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'private': True,
|
'private': True,
|
||||||
'required': True,
|
'regex': (r'^xox[abp]-[a-z0-9-]+$', 'i'),
|
||||||
'regex': (r'^xox[abp]-[A-Z0-9-]+$', 'i'),
|
|
||||||
},
|
},
|
||||||
# Token required as part of the Webhook request
|
# Token required as part of the Webhook request
|
||||||
# /AAAAAAAAA/........./........................
|
# AAAAAAAAA/BBBBBBBBB/CCCCCCCCCCCCCCCCCCCCCCCC
|
||||||
'token_a': {
|
'token': {
|
||||||
'name': _('Token A'),
|
'name': _('Legacy Webhook Token'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'private': True,
|
'private': True,
|
||||||
'regex': (r'^[A-Z0-9]+$', 'i'),
|
'regex': (r'^[a-z0-9]+/[a-z0-9]+/[a-z0-9]+$', 'i'),
|
||||||
},
|
|
||||||
# Token required as part of the Webhook request
|
|
||||||
# /........./BBBBBBBBB/........................
|
|
||||||
'token_b': {
|
|
||||||
'name': _('Token B'),
|
|
||||||
'type': 'string',
|
|
||||||
'private': True,
|
|
||||||
'regex': (r'^[A-Z0-9]+$', 'i'),
|
|
||||||
},
|
|
||||||
# Token required as part of the Webhook request
|
|
||||||
# /........./........./CCCCCCCCCCCCCCCCCCCCCCCC
|
|
||||||
'token_c': {
|
|
||||||
'name': _('Token C'),
|
|
||||||
'type': 'string',
|
|
||||||
'private': True,
|
|
||||||
'regex': (r'^[A-Za-z0-9]+$', 'i'),
|
|
||||||
},
|
},
|
||||||
'target_encoded_id': {
|
'target_encoded_id': {
|
||||||
'name': _('Target Encoded ID'),
|
'name': _('Target Encoded ID'),
|
||||||
@ -272,9 +337,24 @@ class NotifySlack(NotifyBase):
|
|||||||
'to': {
|
'to': {
|
||||||
'alias_of': 'targets',
|
'alias_of': 'targets',
|
||||||
},
|
},
|
||||||
|
'client_id': {
|
||||||
|
'alias_of': 'client_id',
|
||||||
|
},
|
||||||
|
'secret': {
|
||||||
|
'alias_of': 'secret',
|
||||||
|
},
|
||||||
|
'code': {
|
||||||
|
'alias_of': 'code',
|
||||||
|
},
|
||||||
'token': {
|
'token': {
|
||||||
'name': _('Token'),
|
'name': _('Token'),
|
||||||
'alias_of': ('access_token', 'token_a', 'token_b', 'token_c'),
|
'alias_of': ('access_token', 'token'),
|
||||||
|
},
|
||||||
|
'ver': {
|
||||||
|
'name': _('Slack API Version'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': ('v1', 'v2'),
|
||||||
|
'default': 'v1',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -312,9 +392,15 @@ class NotifySlack(NotifyBase):
|
|||||||
r'(?:[ \t]*\|[ \t]*(?:(?P<val>[^\n]+?)[ \t]*)?(?:>|\>)'
|
r'(?:[ \t]*\|[ \t]*(?:(?P<val>[^\n]+?)[ \t]*)?(?:>|\>)'
|
||||||
r'|(?:>|\>)))', re.IGNORECASE)
|
r'|(?:>|\>)))', re.IGNORECASE)
|
||||||
|
|
||||||
def __init__(self, access_token=None, token_a=None, token_b=None,
|
def __init__(self, access_token=None, token=None, targets=None,
|
||||||
token_c=None, targets=None, include_image=True,
|
include_image=None, include_footer=None, use_blocks=None,
|
||||||
include_footer=True, use_blocks=None, **kwargs):
|
ver=None,
|
||||||
|
|
||||||
|
# Entries needed for Webhook - Slack API v2 (2024+)
|
||||||
|
client_id=None, secret=None, code=None,
|
||||||
|
|
||||||
|
# Catch-all
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Slack Object
|
Initialize Slack Object
|
||||||
"""
|
"""
|
||||||
@ -323,39 +409,53 @@ class NotifySlack(NotifyBase):
|
|||||||
# Setup our mode
|
# Setup our mode
|
||||||
self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK
|
self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK
|
||||||
|
|
||||||
if self.mode is SlackMode.WEBHOOK:
|
# v1 Defaults
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
self.token_a = validate_regex(
|
self.token = None
|
||||||
token_a, *self.template_tokens['token_a']['regex'])
|
|
||||||
if not self.token_a:
|
# v2 Defaults
|
||||||
msg = 'An invalid Slack (first) Token ' \
|
self.code = None
|
||||||
'({}) was specified.'.format(token_a)
|
self.client_id = None
|
||||||
|
self.secret = None
|
||||||
|
|
||||||
|
# Get our Slack API Version
|
||||||
|
self.api_ver = SlackAPIVersion.TWO if client_id \
|
||||||
|
and secret and not (token or access_token) and ver is None \
|
||||||
|
else (
|
||||||
|
SLACK_API_VERSION_MAP[NotifySlack.
|
||||||
|
template_args['ver']['default']]
|
||||||
|
if ver is None else next((
|
||||||
|
v for k, v in SLACK_API_VERSION_MAP.items()
|
||||||
|
if str(ver).lower().startswith(k)),
|
||||||
|
SLACK_API_VERSION_MAP[NotifySlack.
|
||||||
|
template_args['ver']['default']]))
|
||||||
|
|
||||||
|
# Depricated Notification
|
||||||
|
if self.api_ver == SlackAPIVersion.ONE:
|
||||||
|
self.logger.deprecate(
|
||||||
|
'Slack Legacy API is set to be deprecated on Mar 31st, 2025. '
|
||||||
|
'You must update your App and/or Bot')
|
||||||
|
|
||||||
|
if self.mode is SlackMode.WEBHOOK:
|
||||||
|
if self.api_ver == SlackAPIVersion.ONE:
|
||||||
|
self.token = validate_regex(
|
||||||
|
token, *self.template_tokens['token']['regex'])
|
||||||
|
if not self.token:
|
||||||
|
msg = 'An invalid Slack Token ' \
|
||||||
|
'({}) was specified.'.format(token)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
self.token_b = validate_regex(
|
else: # version 2
|
||||||
token_b, *self.template_tokens['token_b']['regex'])
|
self.code = code
|
||||||
if not self.token_b:
|
self.client_id = client_id
|
||||||
msg = 'An invalid Slack (second) Token ' \
|
self.secret = secret
|
||||||
'({}) was specified.'.format(token_b)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
self.token_c = validate_regex(
|
else: # Bot
|
||||||
token_c, *self.template_tokens['token_c']['regex'])
|
|
||||||
if not self.token_c:
|
|
||||||
msg = 'An invalid Slack (third) Token ' \
|
|
||||||
'({}) was specified.'.format(token_c)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
else:
|
|
||||||
self.token_a = None
|
|
||||||
self.token_b = None
|
|
||||||
self.token_c = None
|
|
||||||
self.access_token = validate_regex(
|
self.access_token = validate_regex(
|
||||||
access_token, *self.template_tokens['access_token']['regex'])
|
access_token, *self.template_tokens['access_token']['regex'])
|
||||||
if not self.access_token:
|
if not self.access_token:
|
||||||
msg = 'An invalid Slack OAuth Access Token ' \
|
msg = 'An invalid Slack (Bot) OAuth Access Token ' \
|
||||||
'({}) was specified.'.format(access_token)
|
'({}) was specified.'.format(access_token)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
@ -386,12 +486,54 @@ class NotifySlack(NotifyBase):
|
|||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
# Place a thumbnail image inline with the message body
|
# Place a thumbnail image inline with the message body
|
||||||
self.include_image = include_image
|
self.include_image = include_image if include_image is not None \
|
||||||
|
else self.template_args['image']['default']
|
||||||
|
|
||||||
# Place a footer with each post
|
# Place a footer with each post
|
||||||
self.include_footer = include_footer
|
self.include_footer = include_footer if include_footer is not None \
|
||||||
|
else self.template_args['footer']['default']
|
||||||
|
|
||||||
|
# Access token is required with the new 2024 Slack API and
|
||||||
|
# is acquired after authenticating
|
||||||
|
self.__refresh_token = None
|
||||||
|
self.__access_token = None
|
||||||
|
self.__access_token_expiry = datetime.now(timezone.utc)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def authenticate(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Authenticates with Slack API Servers
|
||||||
|
"""
|
||||||
|
|
||||||
|
# First we need to acquire a code
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'scope': ' '.join(self.slack_v2_oauth_scopes),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sharing this code with the user to click on and have a code generated
|
||||||
|
# does not work if there is no valid redirect_uri provided; the
|
||||||
|
# 'out-of-band' on defined above does not work.
|
||||||
|
get_code_url = \
|
||||||
|
f'https://slack.com/oauth/v2/authorize?{urlencode(params)}'
|
||||||
|
|
||||||
|
# The following code does not work (below).
|
||||||
|
# try:
|
||||||
|
# r = requests.get(
|
||||||
|
# get_code_url,
|
||||||
|
# headers=headers,
|
||||||
|
# verify=self.verify_certificate,
|
||||||
|
# timeout=self.request_timeout,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# except requests.RequestException as e:
|
||||||
|
# self.logger.warning(
|
||||||
|
# 'A Connection error occurred acquiring Slack access code.',
|
||||||
|
# )
|
||||||
|
# self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
# # Return; we're done
|
||||||
|
return None
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
@ -401,6 +543,9 @@ class NotifySlack(NotifyBase):
|
|||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
|
if self.api_ver == SlackAPIVersion.TWO:
|
||||||
|
if not self.authenticate():
|
||||||
|
return False
|
||||||
#
|
#
|
||||||
# Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
|
# Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
|
||||||
#
|
#
|
||||||
@ -557,12 +702,7 @@ class NotifySlack(NotifyBase):
|
|||||||
|
|
||||||
# Prepare our Slack URL (depends on mode)
|
# Prepare our Slack URL (depends on mode)
|
||||||
if self.mode is SlackMode.WEBHOOK:
|
if self.mode is SlackMode.WEBHOOK:
|
||||||
url = '{}/{}/{}/{}'.format(
|
url = '{}/{}'.format(self.webhook_url, self.token)
|
||||||
self.webhook_url,
|
|
||||||
self.token_a,
|
|
||||||
self.token_b,
|
|
||||||
self.token_c,
|
|
||||||
)
|
|
||||||
|
|
||||||
else: # SlackMode.BOT
|
else: # SlackMode.BOT
|
||||||
url = self.api_url.format('chat.postMessage')
|
url = self.api_url.format('chat.postMessage')
|
||||||
@ -1029,8 +1169,9 @@ class NotifySlack(NotifyBase):
|
|||||||
here.
|
here.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
self.secure_protocol, self.token_a, self.token_b, self.token_c,
|
self.secure_protocol, self.token, self.access_token,
|
||||||
self.access_token,
|
self.client_id, self.secret,
|
||||||
|
# self.code is intentionally left out
|
||||||
)
|
)
|
||||||
|
|
||||||
def url(self, privacy=False, *args, **kwargs):
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
@ -1043,6 +1184,7 @@ class NotifySlack(NotifyBase):
|
|||||||
'image': 'yes' if self.include_image else 'no',
|
'image': 'yes' if self.include_image else 'no',
|
||||||
'footer': 'yes' if self.include_footer else 'no',
|
'footer': 'yes' if self.include_footer else 'no',
|
||||||
'blocks': 'yes' if self.use_blocks else 'no',
|
'blocks': 'yes' if self.use_blocks else 'no',
|
||||||
|
'ver': SLACK_API_VERSIONS[self.api_ver],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
@ -1056,24 +1198,41 @@ class NotifySlack(NotifyBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.mode == SlackMode.WEBHOOK:
|
if self.mode == SlackMode.WEBHOOK:
|
||||||
return '{schema}://{botname}{token_a}/{token_b}/{token_c}/'\
|
|
||||||
|
if self.api_ver == SlackAPIVersion.ONE:
|
||||||
|
return '{schema}://{botname}{token}/'\
|
||||||
'{targets}/?{params}'.format(
|
'{targets}/?{params}'.format(
|
||||||
schema=self.secure_protocol,
|
schema=self.secure_protocol,
|
||||||
botname=botname,
|
botname=botname,
|
||||||
token_a=self.pprint(self.token_a, privacy, safe=''),
|
token='/'.join(
|
||||||
token_b=self.pprint(self.token_b, privacy, safe=''),
|
[self.pprint(token, privacy, safe='/')
|
||||||
token_c=self.pprint(self.token_c, privacy, safe=''),
|
for token in self.token.split('/')]),
|
||||||
targets='/'.join(
|
targets='/'.join(
|
||||||
[NotifySlack.quote(x, safe='')
|
[NotifySlack.quote(x, safe='')
|
||||||
for x in self.channels]),
|
for x in self.channels]),
|
||||||
params=NotifySlack.urlencode(params),
|
params=NotifySlack.urlencode(params),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return '{schema}://{botname}{client_id}/{secret}{code}'\
|
||||||
|
'{targets}?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
botname=botname,
|
||||||
|
client_id=self.pprint(self.client_id, privacy, safe='/'),
|
||||||
|
secret=self.pprint(self.secret, privacy, safe=''),
|
||||||
|
code='' if not self.code else '/' + self.pprint(
|
||||||
|
self.code, privacy, safe=''),
|
||||||
|
targets=('/' + '/'.join(
|
||||||
|
[NotifySlack.quote(x, safe='')
|
||||||
|
for x in self.channels])) if self.channels else '',
|
||||||
|
params=NotifySlack.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
# else -> self.mode == SlackMode.BOT:
|
# else -> self.mode == SlackMode.BOT:
|
||||||
return '{schema}://{botname}{access_token}/{targets}/'\
|
return '{schema}://{botname}{access_token}/{targets}/'\
|
||||||
'?{params}'.format(
|
'?{params}'.format(
|
||||||
schema=self.secure_protocol,
|
schema=self.secure_protocol,
|
||||||
botname=botname,
|
botname=botname,
|
||||||
access_token=self.pprint(self.access_token, privacy, safe=''),
|
access_token=self.pprint(self.access_token, privacy, safe='/'),
|
||||||
targets='/'.join(
|
targets='/'.join(
|
||||||
[NotifySlack.quote(x, safe='') for x in self.channels]),
|
[NotifySlack.quote(x, safe='') for x in self.channels]),
|
||||||
params=NotifySlack.urlencode(params),
|
params=NotifySlack.urlencode(params),
|
||||||
@ -1098,48 +1257,79 @@ class NotifySlack(NotifyBase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
# The first token is stored in the hostname
|
# The first token is stored in the hostname
|
||||||
token = NotifySlack.unquote(results['host'])
|
results['targets'] = re.split(
|
||||||
|
r'[\s/]+', NotifySlack.unquote(results['host'])) \
|
||||||
|
if results['host'] else []
|
||||||
|
|
||||||
# Get unquoted entries
|
# Get unquoted entries
|
||||||
entries = NotifySlack.split_path(results['fullpath'])
|
results['targets'] += NotifySlack.split_path(results['fullpath'])
|
||||||
|
|
||||||
# Verify if our token_a us a bot token or part of a webhook:
|
# Support Slack API Version
|
||||||
|
if 'ver' in results['qsd'] and len(results['qsd']['ver']):
|
||||||
|
results['ver'] = results['qsd']['ver']
|
||||||
|
|
||||||
|
# Get our values if defined
|
||||||
|
if 'client_id' in results['qsd'] and len(results['qsd']['client_id']):
|
||||||
|
# We're dealing with a Slack v2 API
|
||||||
|
results['client_id'] = results['qsd']['client_id']
|
||||||
|
|
||||||
|
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
|
||||||
|
# We're dealing with a Slack v2 API
|
||||||
|
results['secret'] = results['qsd']['secret']
|
||||||
|
|
||||||
|
if 'code' in results['qsd'] and len(results['qsd']['code']):
|
||||||
|
# We're dealing with a Slack v2 API
|
||||||
|
results['code'] = results['qsd']['code']
|
||||||
|
|
||||||
|
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||||
|
# We're dealing with a Slack v1 API
|
||||||
|
token = NotifySlack.unquote(results['qsd']['token']).strip('/')
|
||||||
|
# check to see if we're dealing with a bot/user token
|
||||||
if token.startswith('xo'):
|
if token.startswith('xo'):
|
||||||
# We're dealing with a bot
|
# We're dealing with a bot
|
||||||
results['access_token'] = token
|
results['access_token'] = token
|
||||||
|
results['token'] = None
|
||||||
else:
|
|
||||||
# We're dealing with a webhook
|
|
||||||
results['token_a'] = token
|
|
||||||
results['token_b'] = entries.pop(0) if entries else None
|
|
||||||
results['token_c'] = entries.pop(0) if entries else None
|
|
||||||
|
|
||||||
# assign remaining entries to the channels we wish to notify
|
|
||||||
results['targets'] = entries
|
|
||||||
|
|
||||||
# Support the token flag where you can set it to the bot token
|
|
||||||
# or the webhook token (with slash delimiters)
|
|
||||||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
|
||||||
# Break our entries up into a list; we can ue the Channel
|
|
||||||
# list delimiter above since it doesn't contain any characters
|
|
||||||
# we don't otherwise accept anyway in our token
|
|
||||||
entries = [x for x in filter(
|
|
||||||
bool, CHANNEL_LIST_DELIM.split(
|
|
||||||
NotifySlack.unquote(results['qsd']['token'])))]
|
|
||||||
|
|
||||||
# check to see if we're dealing with a bot/user token
|
|
||||||
if entries and entries[0].startswith('xo'):
|
|
||||||
# We're dealing with a bot
|
|
||||||
results['access_token'] = entries[0]
|
|
||||||
results['token_a'] = None
|
|
||||||
results['token_b'] = None
|
|
||||||
results['token_c'] = None
|
|
||||||
|
|
||||||
else: # Webhook
|
else: # Webhook
|
||||||
results['access_token'] = None
|
results['access_token'] = None
|
||||||
results['token_a'] = entries.pop(0) if entries else None
|
results['token'] = token
|
||||||
results['token_b'] = entries.pop(0) if entries else None
|
|
||||||
results['token_c'] = entries.pop(0) if entries else None
|
# Verify if our token is a bot token or part of a webhook:
|
||||||
|
if not (results.get('token') or results.get('access_token')
|
||||||
|
or 'client_id' in results or 'secret' in results
|
||||||
|
or 'code' in results) and results['targets'] \
|
||||||
|
and results['targets'][0].startswith('xo'):
|
||||||
|
|
||||||
|
# We're dealing with a bot
|
||||||
|
results['access_token'] = results['targets'].pop(0)
|
||||||
|
results['token'] = None
|
||||||
|
|
||||||
|
elif 'client_id' not in results and results['targets'] \
|
||||||
|
and CLIENT_ID_RE.match(results['targets'][0]):
|
||||||
|
# Store our Client ID
|
||||||
|
results['client_id'] = results['targets'].pop(0)
|
||||||
|
|
||||||
|
else: # parse token from URL if present
|
||||||
|
match = WEBHOOK_RE.match(url)
|
||||||
|
if match:
|
||||||
|
results['access_token'] = None
|
||||||
|
results['token'] = match.group('webhook')
|
||||||
|
# Eliminate webhook entries
|
||||||
|
results['targets'] = results['targets'][3:]
|
||||||
|
|
||||||
|
# We have several entries on our URL and we don't know where they
|
||||||
|
# go. They could also be channels/users/emails
|
||||||
|
if 'client_id' in results and 'secret' not in results:
|
||||||
|
# Acquire secret
|
||||||
|
results['secret'] = \
|
||||||
|
results['targets'].pop(0) if results['targets'] else None
|
||||||
|
|
||||||
|
if 'secret' in results and 'code' not in results \
|
||||||
|
and results['targets'] and \
|
||||||
|
CODE_RE.match(results['targets'][0]):
|
||||||
|
|
||||||
|
# Acquire our code
|
||||||
|
results['code'] = results['targets'].pop(0)
|
||||||
|
|
||||||
# Support the 'to' variable so that we can support rooms this way too
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
# The 'to' makes it easier to use yaml configuration
|
# The 'to' makes it easier to use yaml configuration
|
||||||
@ -1150,7 +1340,8 @@ class NotifySlack(NotifyBase):
|
|||||||
|
|
||||||
# Get Image Flag
|
# Get Image Flag
|
||||||
results['include_image'] = \
|
results['include_image'] = \
|
||||||
parse_bool(results['qsd'].get('image', True))
|
parse_bool(results['qsd'].get(
|
||||||
|
'image', NotifySlack.template_args['image']['default']))
|
||||||
|
|
||||||
# Get Payload structure (use blocks?)
|
# Get Payload structure (use blocks?)
|
||||||
if 'blocks' in results['qsd'] and len(results['qsd']['blocks']):
|
if 'blocks' in results['qsd'] and len(results['qsd']['blocks']):
|
||||||
@ -1158,21 +1349,22 @@ class NotifySlack(NotifyBase):
|
|||||||
|
|
||||||
# Get Footer Flag
|
# Get Footer Flag
|
||||||
results['include_footer'] = \
|
results['include_footer'] = \
|
||||||
parse_bool(results['qsd'].get('footer', True))
|
parse_bool(results['qsd'].get(
|
||||||
|
'footer', NotifySlack.template_args['footer']['default']))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_native_url(url):
|
def parse_native_url(url):
|
||||||
"""
|
"""
|
||||||
Support https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C
|
Legacy Support https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = re.match(
|
result = re.match(
|
||||||
r'^https?://hooks\.slack\.com/services/'
|
r'^https?://hooks\.slack\.com/services/'
|
||||||
r'(?P<token_a>[A-Z0-9]+)/'
|
r'(?P<token_a>[a-z0-9]+)/'
|
||||||
r'(?P<token_b>[A-Z0-9]+)/'
|
r'(?P<token_b>[a-z0-9]+)/'
|
||||||
r'(?P<token_c>[A-Z0-9]+)/?'
|
r'(?P<token_c>[a-z0-9]+)/?'
|
||||||
r'(?P<params>\?.+)?$', url, re.I)
|
r'(?P<params>\?.+)?$', url, re.I)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
@ -1182,7 +1374,7 @@ class NotifySlack(NotifyBase):
|
|||||||
token_a=result.group('token_a'),
|
token_a=result.group('token_a'),
|
||||||
token_b=result.group('token_b'),
|
token_b=result.group('token_b'),
|
||||||
token_c=result.group('token_c'),
|
token_c=result.group('token_c'),
|
||||||
params='' if not result.group('params')
|
params='?ver=1' if not result.group('params')
|
||||||
else result.group('params')))
|
else result.group('params') + '&ver=1'))
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -266,23 +266,11 @@ class AppriseURLTester:
|
|||||||
# from the one that was already created properly
|
# from the one that was already created properly
|
||||||
obj_cmp = Apprise.instantiate(obj.url())
|
obj_cmp = Apprise.instantiate(obj.url())
|
||||||
|
|
||||||
# Our new object should produce the same url identifier
|
|
||||||
if obj.url_identifier != obj_cmp.url_identifier:
|
|
||||||
print('Provided %s' % url)
|
|
||||||
raise AssertionError(
|
|
||||||
"URL Identifier: '{}' != expected '{}'".format(
|
|
||||||
obj_cmp.url_identifier, obj.url_identifier))
|
|
||||||
|
|
||||||
# Back our check up
|
|
||||||
if obj.url_id() != obj_cmp.url_id():
|
|
||||||
print('Provided %s' % url)
|
|
||||||
raise AssertionError(
|
|
||||||
"URL ID(): '{}' != expected '{}'".format(
|
|
||||||
obj_cmp.url_id(), obj.url_id()))
|
|
||||||
|
|
||||||
# Our object should be the same instance as what we had
|
# Our object should be the same instance as what we had
|
||||||
# originally expected above.
|
# originally expected above.
|
||||||
if not isinstance(obj_cmp, NotifyBase):
|
if not isinstance(obj_cmp, NotifyBase):
|
||||||
|
import pdb
|
||||||
|
pdb.set_trace()
|
||||||
# Assert messages are hard to trace back with the
|
# Assert messages are hard to trace back with the
|
||||||
# way these tests work. Just printing before
|
# way these tests work. Just printing before
|
||||||
# throwing our assertion failure makes things
|
# throwing our assertion failure makes things
|
||||||
@ -299,6 +287,20 @@ class AppriseURLTester:
|
|||||||
len(obj_cmp), obj_cmp.url(privacy=True)))
|
len(obj_cmp), obj_cmp.url(privacy=True)))
|
||||||
raise AssertionError("Target miscount %d != %d")
|
raise AssertionError("Target miscount %d != %d")
|
||||||
|
|
||||||
|
# Our new object should produce the same url identifier
|
||||||
|
if obj.url_identifier != obj_cmp.url_identifier:
|
||||||
|
print('Provided %s' % url)
|
||||||
|
raise AssertionError(
|
||||||
|
"URL Identifier: '{}' != expected '{}'".format(
|
||||||
|
obj_cmp.url_identifier, obj.url_identifier))
|
||||||
|
|
||||||
|
# Back our check up
|
||||||
|
if obj.url_id() != obj_cmp.url_id():
|
||||||
|
print('Provided %s' % url)
|
||||||
|
raise AssertionError(
|
||||||
|
"URL ID(): '{}' != expected '{}'".format(
|
||||||
|
obj_cmp.url_id(), obj.url_id()))
|
||||||
|
|
||||||
# Tidy our object
|
# Tidy our object
|
||||||
del obj_cmp
|
del obj_cmp
|
||||||
del instance
|
del instance
|
||||||
|
@ -102,14 +102,14 @@ apprise_url_tests = (
|
|||||||
'requests_response_text': 'ok',
|
'requests_response_text': 'ok',
|
||||||
}),
|
}),
|
||||||
# You can't send to email using webhook
|
# You can't send to email using webhook
|
||||||
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnl/user@gmail.com', {
|
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/user@gmail.com', {
|
||||||
'instance': NotifySlack,
|
'instance': NotifySlack,
|
||||||
'requests_response_text': 'ok',
|
'requests_response_text': 'ok',
|
||||||
# we'll have a notify response failure in this case
|
# we'll have a notify response failure in this case
|
||||||
'notify_response': False,
|
'notify_response': False,
|
||||||
}),
|
}),
|
||||||
# Specify Token on argument string (with username)
|
# Specify Token on argument string (with username)
|
||||||
('slack://bot@_/#nuxref?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnadfdajkjkfl/', {
|
('slack://bot@_/#nuxref?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnadfdajkjkfKl/', {
|
||||||
'instance': NotifySlack,
|
'instance': NotifySlack,
|
||||||
'requests_response_text': 'ok',
|
'requests_response_text': 'ok',
|
||||||
}),
|
}),
|
||||||
@ -452,15 +452,12 @@ def test_plugin_slack_webhook_mode(mock_request):
|
|||||||
mock_request.return_value.text = 'ok'
|
mock_request.return_value.text = 'ok'
|
||||||
|
|
||||||
# Initialize some generic (but valid) tokens
|
# Initialize some generic (but valid) tokens
|
||||||
token_a = 'A' * 9
|
token = '{}/{}/{}'.format('A' * 9, 'B' * 9, 'c' * 24)
|
||||||
token_b = 'B' * 9
|
|
||||||
token_c = 'c' * 24
|
|
||||||
|
|
||||||
# Support strings
|
# Support strings
|
||||||
channels = 'chan1,#chan2,+BAK4K23G5,@user,,,'
|
channels = 'chan1,#chan2,+BAK4K23G5,@user,,,'
|
||||||
|
|
||||||
obj = NotifySlack(
|
obj = NotifySlack(token=token, targets=channels)
|
||||||
token_a=token_a, token_b=token_b, token_c=token_c, targets=channels)
|
|
||||||
assert len(obj.channels) == 4
|
assert len(obj.channels) == 4
|
||||||
|
|
||||||
# This call includes an image with it's payload:
|
# This call includes an image with it's payload:
|
||||||
@ -469,14 +466,10 @@ def test_plugin_slack_webhook_mode(mock_request):
|
|||||||
|
|
||||||
# Missing first Token
|
# Missing first Token
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
NotifySlack(
|
NotifySlack(token=None)
|
||||||
token_a=None, token_b=token_b, token_c=token_c,
|
|
||||||
targets=channels)
|
|
||||||
|
|
||||||
# Test include_image
|
# Test include_image
|
||||||
obj = NotifySlack(
|
obj = NotifySlack(token=token, targets=channels, include_image=True)
|
||||||
token_a=token_a, token_b=token_b, token_c=token_c, targets=channels,
|
|
||||||
include_image=True)
|
|
||||||
|
|
||||||
# This call includes an image with it's payload:
|
# This call includes an image with it's payload:
|
||||||
assert obj.notify(
|
assert obj.notify(
|
||||||
|
Loading…
Reference in New Issue
Block a user