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