1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com> 4# All rights reserved. 5# 6# This code is licensed under the MIT License. 7# 8# Permission is hereby granted, free of charge, to any person obtaining a copy 9# of this software and associated documentation files(the "Software"), to deal 10# in the Software without restriction, including without limitation the rights 11# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12# copies of the Software, and to permit persons to whom the Software is 13# furnished to do so, subject to the following conditions : 14# 15# The above copyright notice and this permission notice shall be included in 16# all copies or substantial portions of the Software. 17# 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24# THE SOFTWARE. 25 26# For this to work correctly you need to create a webhook. You'll also 27# need a GSuite account (there are free trials if you don't have one) 28# 29# - Open Google Chat in your browser: 30# Link: https://chat.google.com/ 31# - Go to the room to which you want to add a bot. 32# - From the room menu at the top of the page, select Manage webhooks. 33# - Provide it a name and optional avatar and click SAVE 34# - Copy the URL listed next to your new webhook in the Webhook URL column. 35# - Click outside the dialog box to close. 36# 37# When you've completed, you'll get a URL that looks a little like this: 38# https://chat.googleapis.com/v1/spaces/AAAAk6lGXyM/\ 39# messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&\ 40# token=O7b1nyri_waOpLMSzbFILAGRzgtQofPW71fEEXKcyFk%3D 41# 42# Simplified, it looks like this: 43# https://chat.googleapis.com/v1/spaces/WORKSPACE/messages?\ 44# key=WEBHOOK_KEY&token=WEBHOOK_TOKEN 45# 46# This plugin will simply work using the url of: 47# gchat://WORKSPACE/WEBHOOK_KEY/WEBHOOK_TOKEN 48# 49# API Documentation on Webhooks: 50# - https://developers.google.com/hangouts/chat/quickstart/\ 51# incoming-bot-python 52# - https://developers.google.com/hangouts/chat/reference/rest 53# 54import re 55import requests 56from json import dumps 57 58from .NotifyBase import NotifyBase 59from ..common import NotifyFormat 60from ..common import NotifyType 61from ..utils import validate_regex 62from ..AppriseLocale import gettext_lazy as _ 63 64 65class NotifyGoogleChat(NotifyBase): 66 """ 67 A wrapper to Google Chat Notifications 68 69 """ 70 # The default descriptive name associated with the Notification 71 service_name = 'Google Chat' 72 73 # The services URL 74 service_url = 'https://chat.google.com/' 75 76 # The default secure protocol 77 secure_protocol = 'gchat' 78 79 # A URL that takes you to the setup/help of the specific protocol 80 setup_url = 'https://github.com/caronc/apprise/wiki/Notify_googlechat' 81 82 # Google Chat Webhook 83 notify_url = 'https://chat.googleapis.com/v1/spaces/{workspace}/messages' \ 84 '?key={key}&token={token}' 85 86 # Default Notify Format 87 notify_format = NotifyFormat.MARKDOWN 88 89 # A title can not be used for Google Chat Messages. Setting this to zero 90 # will cause any title (if defined) to get placed into the message body. 91 title_maxlen = 0 92 93 # The maximum allowable characters allowed in the body per message 94 body_maxlen = 4000 95 96 # Define object templates 97 templates = ( 98 '{schema}://{workspace}/{webhook_key}/{webhook_token}', 99 ) 100 101 # Define our template tokens 102 template_tokens = dict(NotifyBase.template_tokens, **{ 103 'workspace': { 104 'name': _('Workspace'), 105 'type': 'string', 106 'private': True, 107 'required': True, 108 }, 109 'webhook_key': { 110 'name': _('Webhook Key'), 111 'type': 'string', 112 'private': True, 113 'required': True, 114 }, 115 'webhook_token': { 116 'name': _('Webhook Token'), 117 'type': 'string', 118 'private': True, 119 'required': True, 120 }, 121 }) 122 123 # Define our template arguments 124 template_args = dict(NotifyBase.template_args, **{ 125 'workspace': { 126 'alias_of': 'workspace', 127 }, 128 'key': { 129 'alias_of': 'webhook_key', 130 }, 131 'token': { 132 'alias_of': 'webhook_token', 133 }, 134 }) 135 136 def __init__(self, workspace, webhook_key, webhook_token, **kwargs): 137 """ 138 Initialize Google Chat Object 139 140 """ 141 super(NotifyGoogleChat, self).__init__(**kwargs) 142 143 # Workspace (associated with project) 144 self.workspace = validate_regex(workspace) 145 if not self.workspace: 146 msg = 'An invalid Google Chat Workspace ' \ 147 '({}) was specified.'.format(workspace) 148 self.logger.warning(msg) 149 raise TypeError(msg) 150 151 # Webhook Key (associated with project) 152 self.webhook_key = validate_regex(webhook_key) 153 if not self.webhook_key: 154 msg = 'An invalid Google Chat Webhook Key ' \ 155 '({}) was specified.'.format(webhook_key) 156 self.logger.warning(msg) 157 raise TypeError(msg) 158 159 # Webhook Token (associated with project) 160 self.webhook_token = validate_regex(webhook_token) 161 if not self.webhook_token: 162 msg = 'An invalid Google Chat Webhook Token ' \ 163 '({}) was specified.'.format(webhook_token) 164 self.logger.warning(msg) 165 raise TypeError(msg) 166 167 return 168 169 def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 170 """ 171 Perform Google Chat Notification 172 """ 173 174 # Our headers 175 headers = { 176 'User-Agent': self.app_id, 177 'Content-Type': 'application/json; charset=utf-8', 178 } 179 180 payload = { 181 # Our Message 182 'text': body, 183 } 184 185 # Construct Notify URL 186 notify_url = self.notify_url.format( 187 workspace=self.workspace, 188 key=self.webhook_key, 189 token=self.webhook_token, 190 ) 191 192 self.logger.debug('Google Chat POST URL: %s (cert_verify=%r)' % ( 193 notify_url, self.verify_certificate, 194 )) 195 self.logger.debug('Google Chat Payload: %s' % str(payload)) 196 197 # Always call throttle before any remote server i/o is made 198 self.throttle() 199 try: 200 r = requests.post( 201 notify_url, 202 data=dumps(payload), 203 headers=headers, 204 verify=self.verify_certificate, 205 timeout=self.request_timeout, 206 ) 207 if r.status_code not in ( 208 requests.codes.ok, requests.codes.no_content): 209 210 # We had a problem 211 status_str = \ 212 NotifyBase.http_response_code_lookup(r.status_code) 213 214 self.logger.warning( 215 'Failed to send Google Chat notification: ' 216 '{}{}error={}.'.format( 217 status_str, 218 ', ' if status_str else '', 219 r.status_code)) 220 221 self.logger.debug('Response Details:\r\n{}'.format(r.content)) 222 223 # Return; we're done 224 return False 225 226 else: 227 self.logger.info('Sent Google Chat notification.') 228 229 except requests.RequestException as e: 230 self.logger.warning( 231 'A Connection error occurred postingto Google Chat.') 232 self.logger.debug('Socket Exception: %s' % str(e)) 233 return False 234 235 return True 236 237 def url(self, privacy=False, *args, **kwargs): 238 """ 239 Returns the URL built dynamically based on specified arguments. 240 """ 241 242 # Set our parameters 243 params = self.url_parameters(privacy=privacy, *args, **kwargs) 244 245 return '{schema}://{workspace}/{key}/{token}/?{params}'.format( 246 schema=self.secure_protocol, 247 workspace=self.pprint(self.workspace, privacy, safe=''), 248 key=self.pprint(self.webhook_key, privacy, safe=''), 249 token=self.pprint(self.webhook_token, privacy, safe=''), 250 params=NotifyGoogleChat.urlencode(params), 251 ) 252 253 @staticmethod 254 def parse_url(url): 255 """ 256 Parses the URL and returns enough arguments that can allow 257 us to re-instantiate this object. 258 259 Syntax: 260 gchat://workspace/webhook_key/webhook_token 261 262 """ 263 results = NotifyBase.parse_url(url, verify_host=False) 264 if not results: 265 # We're done early as we couldn't load the results 266 return results 267 268 # Store our Workspace 269 results['workspace'] = NotifyGoogleChat.unquote(results['host']) 270 271 # Acquire our tokens 272 tokens = NotifyGoogleChat.split_path(results['fullpath']) 273 274 # Store our Webhook Key 275 results['webhook_key'] = tokens.pop(0) if tokens else None 276 277 # Store our Webhook Token 278 results['webhook_token'] = tokens.pop(0) if tokens else None 279 280 # Support arguments as overrides (if specified) 281 if 'workspace' in results['qsd']: 282 results['workspace'] = \ 283 NotifyGoogleChat.unquote(results['qsd']['workspace']) 284 285 if 'key' in results['qsd']: 286 results['webhook_key'] = \ 287 NotifyGoogleChat.unquote(results['qsd']['key']) 288 289 if 'token' in results['qsd']: 290 results['webhook_token'] = \ 291 NotifyGoogleChat.unquote(results['qsd']['token']) 292 293 return results 294 295 @staticmethod 296 def parse_native_url(url): 297 """ 298 Support 299 https://chat.googleapis.com/v1/spaces/{workspace}/messages 300 '?key={key}&token={token} 301 """ 302 303 result = re.match( 304 r'^https://chat\.googleapis\.com/v1/spaces/' 305 r'(?P<workspace>[A-Z0-9_-]+)/messages/*(?P<params>.+)$', 306 url, re.I) 307 308 if result: 309 return NotifyGoogleChat.parse_url( 310 '{schema}://{workspace}/{params}'.format( 311 schema=NotifyGoogleChat.secure_protocol, 312 workspace=result.group('workspace'), 313 params=result.group('params'))) 314 315 return None 316