From fc16c7bf0b0e243318baa470b16df56987bf4a1f Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 12 Nov 2022 15:14:54 -0500 Subject: [PATCH] Google Chat Thread-Key support (#753) --- apprise/plugins/NotifyGoogleChat.py | 59 +++++++++++++++++-- test/test_plugin_google_chat.py | 90 ++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/apprise/plugins/NotifyGoogleChat.py b/apprise/plugins/NotifyGoogleChat.py index 7784e492..6a65ba7f 100644 --- a/apprise/plugins/NotifyGoogleChat.py +++ b/apprise/plugins/NotifyGoogleChat.py @@ -80,8 +80,7 @@ class NotifyGoogleChat(NotifyBase): setup_url = 'https://github.com/caronc/apprise/wiki/Notify_googlechat' # Google Chat Webhook - notify_url = 'https://chat.googleapis.com/v1/spaces/{workspace}/messages' \ - '?key={key}&token={token}' + notify_url = 'https://chat.googleapis.com/v1/spaces/{workspace}/messages' # Default Notify Format notify_format = NotifyFormat.MARKDOWN @@ -96,6 +95,7 @@ class NotifyGoogleChat(NotifyBase): # Define object templates templates = ( '{schema}://{workspace}/{webhook_key}/{webhook_token}', + '{schema}://{workspace}/{webhook_key}/{webhook_token}/{thread_key}', ) # Define our template tokens @@ -118,6 +118,11 @@ class NotifyGoogleChat(NotifyBase): 'private': True, 'required': True, }, + 'thread_key': { + 'name': _('Thread Key'), + 'type': 'string', + 'private': True, + }, }) # Define our template arguments @@ -131,9 +136,13 @@ class NotifyGoogleChat(NotifyBase): '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 @@ -164,6 +173,16 @@ class NotifyGoogleChat(NotifyBase): self.logger.warning(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 def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -185,13 +204,21 @@ class NotifyGoogleChat(NotifyBase): # Construct Notify URL notify_url = self.notify_url.format( 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)' % ( notify_url, self.verify_certificate, )) + self.logger.debug('Google Chat Parameters: %s' % str(params)) self.logger.debug('Google Chat Payload: %s' % str(payload)) # Always call throttle before any remote server i/o is made @@ -199,6 +226,7 @@ class NotifyGoogleChat(NotifyBase): try: r = requests.post( notify_url, + params=params, data=dumps(payload), headers=headers, verify=self.verify_certificate, @@ -242,11 +270,13 @@ class NotifyGoogleChat(NotifyBase): # Set our parameters 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, workspace=self.pprint(self.workspace, privacy, safe=''), key=self.pprint(self.webhook_key, 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), ) @@ -258,6 +288,7 @@ class NotifyGoogleChat(NotifyBase): Syntax: gchat://workspace/webhook_key/webhook_token + gchat://workspace/webhook_key/webhook_token/thread_key """ results = NotifyBase.parse_url(url, verify_host=False) @@ -277,6 +308,9 @@ class NotifyGoogleChat(NotifyBase): # Store our Webhook Token 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) if 'workspace' in results['qsd']: results['workspace'] = \ @@ -290,6 +324,17 @@ class NotifyGoogleChat(NotifyBase): results['webhook_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 @staticmethod @@ -298,6 +343,8 @@ class NotifyGoogleChat(NotifyBase): Support https://chat.googleapis.com/v1/spaces/{workspace}/messages '?key={key}&token={token} + https://chat.googleapis.com/v1/spaces/{workspace}/messages + '?key={key}&token={token}&threadKey={thread} """ result = re.match( diff --git a/test/test_plugin_google_chat.py b/test/test_plugin_google_chat.py index b07e69ca..6354a33d 100644 --- a/test/test_plugin_google_chat.py +++ b/test/test_plugin_google_chat.py @@ -23,9 +23,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import requests - +import pytest +from apprise import Apprise from apprise.plugins.NotifyGoogleChat import NotifyGoogleChat from helpers import AppriseURLTester +from unittest import mock +from apprise import NotifyType +from json import loads # Disable logging for a cleaner testing output import logging @@ -57,12 +61,25 @@ apprise_url_tests = ( 'instance': NotifyGoogleChat, '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' '?key=mykey&token=mytoken', { 'instance': NotifyGoogleChat, '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', { 'instance': NotifyGoogleChat, # force a failure @@ -92,3 +109,70 @@ def test_plugin_google_chat_urls(): # Run our general tests 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())