1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2020 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
26from __future__ import absolute_import
27from __future__ import print_function
28
29import platform
30import subprocess
31import os
32
33from .NotifyBase import NotifyBase
34from ..common import NotifyImageSize
35from ..common import NotifyType
36from ..utils import parse_bool
37from ..AppriseLocale import gettext_lazy as _
38
39# Default our global support flag
40NOTIFY_MACOSX_SUPPORT_ENABLED = False
41
42if platform.system() == 'Darwin':
43    # Check this is Mac OS X 10.8, or higher
44    major, minor = platform.mac_ver()[0].split('.')[:2]
45
46    # Toggle our enabled flag if verion is correct and executable
47    # found. This is done in such a way to provide verbosity to the
48    # end user so they know why it may or may not work for them.
49    NOTIFY_MACOSX_SUPPORT_ENABLED = \
50        (int(major) > 10 or (int(major) == 10 and int(minor) >= 8))
51
52
53class NotifyMacOSX(NotifyBase):
54    """
55    A wrapper for the MacOS X terminal-notifier tool
56
57    Source: https://github.com/julienXX/terminal-notifier
58    """
59
60    # Set our global enabled flag
61    enabled = NOTIFY_MACOSX_SUPPORT_ENABLED
62
63    requirements = {
64        # Define our required packaging in order to work
65        'details': _(
66            'Only works with Mac OS X 10.8 and higher. Additionally '
67            ' requires that /usr/local/bin/terminal-notifier is locally '
68            'accessible.')
69    }
70
71    # The default descriptive name associated with the Notification
72    service_name = _('MacOSX Notification')
73
74    # The services URL
75    service_url = 'https://github.com/julienXX/terminal-notifier'
76
77    # The default protocol
78    protocol = 'macosx'
79
80    # A URL that takes you to the setup/help of the specific protocol
81    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_macosx'
82
83    # Allows the user to specify the NotifyImageSize object
84    image_size = NotifyImageSize.XY_128
85
86    # Disable throttle rate for MacOSX requests since they are normally
87    # local anyway
88    request_rate_per_sec = 0
89
90    # Limit results to just the first 10 line otherwise there is just to much
91    # content to display
92    body_max_line_count = 10
93
94    # The path to the terminal-notifier
95    notify_path = '/usr/local/bin/terminal-notifier'
96
97    # Define object templates
98    templates = (
99        '{schema}://',
100    )
101
102    # Define our template arguments
103    template_args = dict(NotifyBase.template_args, **{
104        'image': {
105            'name': _('Include Image'),
106            'type': 'bool',
107            'default': True,
108            'map_to': 'include_image',
109        },
110        # Play the NAME sound when the notification appears.
111        # Sound names are listed in Sound Preferences.
112        # Use 'default' for the default sound.
113        'sound': {
114            'name': _('Sound'),
115            'type': 'string',
116        },
117    })
118
119    def __init__(self, sound=None, include_image=True, **kwargs):
120        """
121        Initialize MacOSX Object
122        """
123
124        super(NotifyMacOSX, self).__init__(**kwargs)
125
126        # Track whether or not we want to send an image with our notification
127        # or not.
128        self.include_image = include_image
129
130        # Set sound object (no q/a for now)
131        self.sound = sound
132        return
133
134    def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
135        """
136        Perform MacOSX Notification
137        """
138
139        if not os.access(self.notify_path, os.X_OK):
140            self.logger.warning(
141                "MacOSX Notifications require '{}' to be in place."
142                .format(self.notify_path))
143            return False
144
145        # Start with our notification path
146        cmd = [
147            self.notify_path,
148            '-message', body,
149        ]
150
151        # Title is an optional switch
152        if title:
153            cmd.extend(['-title', title])
154
155        # The sound to play
156        if self.sound:
157            cmd.extend(['-sound', self.sound])
158
159        # Support any defined images if set
160        image_path = None if not self.include_image \
161            else self.image_url(notify_type)
162        if image_path:
163            cmd.extend(['-appIcon', image_path])
164
165        # Always call throttle before any remote server i/o is made
166        self.throttle()
167
168        # Capture some output for helpful debugging later on
169        self.logger.debug('MacOSX CMD: {}'.format(' '.join(cmd)))
170
171        # Send our notification
172        output = subprocess.Popen(
173            cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
174
175        # Wait for process to complete
176        output.wait()
177
178        if output.returncode:
179            self.logger.warning('Failed to send MacOSX notification.')
180            self.logger.exception('MacOSX Exception')
181            return False
182
183        self.logger.info('Sent MacOSX notification.')
184        return True
185
186    def url(self, privacy=False, *args, **kwargs):
187        """
188        Returns the URL built dynamically based on specified arguments.
189        """
190
191        # Define any URL parametrs
192        params = {
193            'image': 'yes' if self.include_image else 'no',
194        }
195
196        # Extend our parameters
197        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
198
199        if self.sound:
200            # Store our sound
201            params['sound'] = self.sound
202
203        return '{schema}://_/?{params}'.format(
204            schema=self.protocol,
205            params=NotifyMacOSX.urlencode(params),
206        )
207
208    @staticmethod
209    def parse_url(url):
210        """
211        There are no parameters nessisary for this protocol; simply having
212        gnome:// is all you need.  This function just makes sure that
213        is in place.
214
215        """
216
217        results = NotifyBase.parse_url(url, verify_host=False)
218
219        # Include images with our message
220        results['include_image'] = \
221            parse_bool(results['qsd'].get('image', True))
222
223        # Support 'sound'
224        if 'sound' in results['qsd'] and len(results['qsd']['sound']):
225            results['sound'] = NotifyMacOSX.unquote(results['qsd']['sound'])
226
227        return results
228