Google Chat Thread-Key support (#753)

This commit is contained in:
Chris Caron 2022-11-12 15:14:54 -05:00 committed by GitHub
parent 2912dfd1e3
commit fc16c7bf0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 140 additions and 9 deletions

View File

@ -80,8 +80,7 @@ class NotifyGoogleChat(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_googlechat' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_googlechat'
# Google Chat Webhook # Google Chat Webhook
notify_url = 'https://chat.googleapis.com/v1/spaces/{workspace}/messages' \ notify_url = 'https://chat.googleapis.com/v1/spaces/{workspace}/messages'
'?key={key}&token={token}'
# Default Notify Format # Default Notify Format
notify_format = NotifyFormat.MARKDOWN notify_format = NotifyFormat.MARKDOWN
@ -96,6 +95,7 @@ class NotifyGoogleChat(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{workspace}/{webhook_key}/{webhook_token}', '{schema}://{workspace}/{webhook_key}/{webhook_token}',
'{schema}://{workspace}/{webhook_key}/{webhook_token}/{thread_key}',
) )
# Define our template tokens # Define our template tokens
@ -118,6 +118,11 @@ class NotifyGoogleChat(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'thread_key': {
'name': _('Thread Key'),
'type': 'string',
'private': True,
},
}) })
# Define our template arguments # Define our template arguments
@ -131,9 +136,13 @@ class NotifyGoogleChat(NotifyBase):
'token': { 'token': {
'alias_of': 'webhook_token', 'alias_of': 'webhook_token',
}, },
'thread': {
'alias_of': 'thread_key',
},
}) })
def __init__(self, workspace, webhook_key, webhook_token, **kwargs): def __init__(self, workspace, webhook_key, webhook_token,
thread_key=None, **kwargs):
""" """
Initialize Google Chat Object Initialize Google Chat Object
@ -164,6 +173,16 @@ class NotifyGoogleChat(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if thread_key:
self.thread_key = validate_regex(thread_key)
if not self.thread_key:
msg = 'An invalid Google Chat Thread Key ' \
'({}) was specified.'.format(thread_key)
self.logger.warning(msg)
raise TypeError(msg)
else:
self.thread_key = None
return return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -185,13 +204,21 @@ class NotifyGoogleChat(NotifyBase):
# Construct Notify URL # Construct Notify URL
notify_url = self.notify_url.format( notify_url = self.notify_url.format(
workspace=self.workspace, workspace=self.workspace,
key=self.webhook_key,
token=self.webhook_token,
) )
params = {
# Prepare our URL Parameters
'token': self.webhook_token,
'key': self.webhook_key,
}
if self.thread_key:
params['threadKey'] = self.thread_key
self.logger.debug('Google Chat POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Google Chat POST URL: %s (cert_verify=%r)' % (
notify_url, self.verify_certificate, notify_url, self.verify_certificate,
)) ))
self.logger.debug('Google Chat Parameters: %s' % str(params))
self.logger.debug('Google Chat Payload: %s' % str(payload)) self.logger.debug('Google Chat Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made # Always call throttle before any remote server i/o is made
@ -199,6 +226,7 @@ class NotifyGoogleChat(NotifyBase):
try: try:
r = requests.post( r = requests.post(
notify_url, notify_url,
params=params,
data=dumps(payload), data=dumps(payload),
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
@ -242,11 +270,13 @@ class NotifyGoogleChat(NotifyBase):
# Set our parameters # Set our parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs) params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{workspace}/{key}/{token}/?{params}'.format( return '{schema}://{workspace}/{key}/{token}/{thread}?{params}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
workspace=self.pprint(self.workspace, privacy, safe=''), workspace=self.pprint(self.workspace, privacy, safe=''),
key=self.pprint(self.webhook_key, privacy, safe=''), key=self.pprint(self.webhook_key, privacy, safe=''),
token=self.pprint(self.webhook_token, privacy, safe=''), token=self.pprint(self.webhook_token, privacy, safe=''),
thread='' if not self.thread_key
else self.pprint(self.thread_key, privacy, safe=''),
params=NotifyGoogleChat.urlencode(params), params=NotifyGoogleChat.urlencode(params),
) )
@ -258,6 +288,7 @@ class NotifyGoogleChat(NotifyBase):
Syntax: Syntax:
gchat://workspace/webhook_key/webhook_token gchat://workspace/webhook_key/webhook_token
gchat://workspace/webhook_key/webhook_token/thread_key
""" """
results = NotifyBase.parse_url(url, verify_host=False) results = NotifyBase.parse_url(url, verify_host=False)
@ -277,6 +308,9 @@ class NotifyGoogleChat(NotifyBase):
# Store our Webhook Token # Store our Webhook Token
results['webhook_token'] = tokens.pop(0) if tokens else None results['webhook_token'] = tokens.pop(0) if tokens else None
# Store our Thread Key
results['thread_key'] = tokens.pop(0) if tokens else None
# Support arguments as overrides (if specified) # Support arguments as overrides (if specified)
if 'workspace' in results['qsd']: if 'workspace' in results['qsd']:
results['workspace'] = \ results['workspace'] = \
@ -290,6 +324,17 @@ class NotifyGoogleChat(NotifyBase):
results['webhook_token'] = \ results['webhook_token'] = \
NotifyGoogleChat.unquote(results['qsd']['token']) NotifyGoogleChat.unquote(results['qsd']['token'])
if 'thread' in results['qsd']:
results['thread_key'] = \
NotifyGoogleChat.unquote(results['qsd']['thread'])
elif 'threadkey' in results['qsd']:
# Support Google Chat's Thread Key (if set)
# keys are always made lowercase; so check above is attually
# testing threadKey successfully as well
results['thread_key'] = \
NotifyGoogleChat.unquote(results['qsd']['threadkey'])
return results return results
@staticmethod @staticmethod
@ -298,6 +343,8 @@ class NotifyGoogleChat(NotifyBase):
Support Support
https://chat.googleapis.com/v1/spaces/{workspace}/messages https://chat.googleapis.com/v1/spaces/{workspace}/messages
'?key={key}&token={token} '?key={key}&token={token}
https://chat.googleapis.com/v1/spaces/{workspace}/messages
'?key={key}&token={token}&threadKey={thread}
""" """
result = re.match( result = re.match(

View File

@ -23,9 +23,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import requests import requests
import pytest
from apprise import Apprise
from apprise.plugins.NotifyGoogleChat import NotifyGoogleChat from apprise.plugins.NotifyGoogleChat import NotifyGoogleChat
from helpers import AppriseURLTester from helpers import AppriseURLTester
from unittest import mock
from apprise import NotifyType
from json import loads
# Disable logging for a cleaner testing output # Disable logging for a cleaner testing output
import logging import logging
@ -57,12 +61,25 @@ apprise_url_tests = (
'instance': NotifyGoogleChat, 'instance': NotifyGoogleChat,
'privacy_url': 'gchat://w...s/m...y/m...n', 'privacy_url': 'gchat://w...s/m...y/m...n',
}), }),
# Google Native Webhohok URL ('gchat://?workspace=ws&key=mykey&token=mytoken&thread=abc123', {
# Test our thread key
'instance': NotifyGoogleChat,
'privacy_url': 'gchat://w...s/m...y/m...n/a...3',
}),
('gchat://?workspace=ws&key=mykey&token=mytoken&threadKey=abc345', {
# Test our thread key
'instance': NotifyGoogleChat,
'privacy_url': 'gchat://w...s/m...y/m...n/a...5',
}),
# Google Native Webhook URL
('https://chat.googleapis.com/v1/spaces/myworkspace/messages' ('https://chat.googleapis.com/v1/spaces/myworkspace/messages'
'?key=mykey&token=mytoken', { '?key=mykey&token=mytoken', {
'instance': NotifyGoogleChat, 'instance': NotifyGoogleChat,
'privacy_url': 'gchat://m...e/m...y/m...n'}), 'privacy_url': 'gchat://m...e/m...y/m...n'}),
('https://chat.googleapis.com/v1/spaces/myworkspace/messages'
'?key=mykey&token=mytoken&threadKey=mythreadkey', {
'instance': NotifyGoogleChat,
'privacy_url': 'gchat://m...e/m...y/m...n/m...y'}),
('gchat://workspace/key/token', { ('gchat://workspace/key/token', {
'instance': NotifyGoogleChat, 'instance': NotifyGoogleChat,
# force a failure # force a failure
@ -92,3 +109,70 @@ def test_plugin_google_chat_urls():
# Run our general tests # Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all() AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_google_chat_general(mock_post):
"""
NotifyGoogleChat() General Checks
"""
# Initialize some generic (but valid) tokens
workspace = 'ws'
key = 'key'
threadkey = 'threadkey'
token = 'token'
# Prepare Mock
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
# Test our messaging
obj = Apprise.instantiate(
'gchat://{}/{}/{}'.format(workspace, key, token))
assert isinstance(obj, NotifyGoogleChat)
assert obj.notify(
body="test body", title='title',
notify_type=NotifyType.INFO) is True
# Test our call count
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://chat.googleapis.com/v1/spaces/ws/messages'
params = mock_post.call_args_list[0][1]['params']
assert params.get('token') == token
assert params.get('key') == key
assert 'threadKey' not in params
payload = loads(mock_post.call_args_list[0][1]['data'])
assert payload['text'] == "title\r\ntest body"
mock_post.reset_mock()
# Test our messaging with the threadKey
obj = Apprise.instantiate(
'gchat://{}/{}/{}/{}'.format(workspace, key, token, threadkey))
assert isinstance(obj, NotifyGoogleChat)
assert obj.notify(
body="test body", title='title',
notify_type=NotifyType.INFO) is True
# Test our call count
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://chat.googleapis.com/v1/spaces/ws/messages'
params = mock_post.call_args_list[0][1]['params']
assert params.get('token') == token
assert params.get('key') == key
assert params.get('threadKey') == threadkey
payload = loads(mock_post.call_args_list[0][1]['data'])
assert payload['text'] == "title\r\ntest body"
def test_plugin_google_chat_edge_case():
"""
NotifyGoogleChat() Edge Cases
"""
with pytest.raises(TypeError):
NotifyGoogleChat('workspace', 'webhook', 'token', thread_key=object())