diff --git a/README b/README index baf27c05..2e654168 100644 --- a/README +++ b/README @@ -36,6 +36,9 @@ The table below identifies the services this tool supports and some example serv * Faast faast://authorizationtoken +* Gnome Notifications + gnome:// + * Growl growl://hostname growl://hostname:portno diff --git a/README.md b/README.md index 0a12fec0..317e2ffa 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The table below identifies the services this tool supports and some example serv | [Discord](https://github.com/caronc/apprise/wiki/Notify_discord) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token
discord://avatar@webhook_id/webhook_token | [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/
emby://user:password@hostname | [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken +| [Gnome](https://github.com/caronc/apprise/wiki/Notify_gnome) | gnome:// | n/a | gnome:// | [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port
**Note**: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1 | [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/EventToTrigger
ifttt://webhooksID/EventToTrigger/Value1/Value2/Value3
ifttt://webhooksID/EventToTrigger/?Value3=NewEntry&Value2=AnotherValue | [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/ diff --git a/apprise/plugins/NotifyGnome.py b/apprise/plugins/NotifyGnome.py new file mode 100644 index 00000000..b33276ef --- /dev/null +++ b/apprise/plugins/NotifyGnome.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# +# Gnome Notify Wrapper +# +# Copyright (C) 2019 Chris Caron +# +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import +from __future__ import print_function + +import re + +from .NotifyBase import NotifyBase +from ..common import NotifyImageSize + +# Default our global support flag +NOTIFY_GNOME_SUPPORT_ENABLED = False + +try: + # 3rd party modules (Gnome Only) + import gi + + # require_version() call is required otherwise we generate a warning + gi.require_version("Notify", "0.7") + + # We can import the actual libraries we care about now: + from gi.repository import Notify + from gi.repository import GdkPixbuf + + # We're good to go! + NOTIFY_GNOME_SUPPORT_ENABLED = True + +except ImportError: + # No problem; we just simply can't support this plugin; we could + # be in microsoft windows, or we just don't have the python-gobject + # library available to us (or maybe one we don't support)? + pass + + +# Urgencies +class GnomeUrgency(object): + LOW = 0 + NORMAL = 1 + HIGH = 2 + + +GNOME_URGENCIES = ( + GnomeUrgency.LOW, + GnomeUrgency.NORMAL, + GnomeUrgency.HIGH, +) + + +class NotifyGnome(NotifyBase): + """ + A wrapper for local Gnome Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Gnome Notification' + + # The default protocol + protocol = 'gnome' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gnome' + + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_128 + + # This entry is a bit hacky, but it allows us to unit-test this library + # in an environment that simply doesn't have the gnome packages + # available to us. It also allows us to handle situations where the + # packages actually are present but we need to test that they aren't. + # If anyone is seeing this had knows a better way of testing this + # outside of what is defined in test/test_gnome_plugin.py, please + # let me know! :) + _enabled = NOTIFY_GNOME_SUPPORT_ENABLED + + def __init__(self, urgency=None, **kwargs): + """ + Initialize Gnome Object + """ + + super(NotifyGnome, self).__init__(**kwargs) + + # The urgency of the message + if urgency not in GNOME_URGENCIES: + self.urgency = GnomeUrgency.NORMAL + + else: + self.urgency = urgency + + def notify(self, title, body, notify_type, **kwargs): + """ + Perform Gnome Notification + """ + + if not self._enabled: + self.logger.warning( + "Gnome Notifications are not supported by this system.") + return False + + # Limit results to just the first 10 line otherwise + # there is just to much content to display + body = re.split('[\r\n]+', body) + if title: + # Place title on first line if it exists + body.insert(0, title) + + body = '\r\n'.join(body[0:10]) + + try: + # App initialization + Notify.init(self.app_id) + + # image path + icon_path = self.image_path(notify_type, extension='.ico') + + # Build message body + notification = Notify.Notification.new(body) + + # Assign urgency + notification.set_urgency(self.urgency) + + try: + # Use Pixbuf to create the proper image type + image = GdkPixbuf.Pixbuf.new_from_file(icon_path) + + # Associate our image to our notification + notification.set_icon_from_pixbuf(image) + notification.set_image_from_pixbuf(image) + + except Exception as e: + self.logger.warning( + "Could not load Gnome notification icon ({}): {}" + .format(icon_path, e)) + + notification.show() + self.logger.info('Sent Gnome notification.') + + except Exception as e: + self.logger.warning('Failed to send Gnome notification.') + self.logger.exception('Gnome Exception') + return False + + return True + + @staticmethod + def parse_url(url): + """ + There are no parameters nessisary for this protocol; simply having + gnome:// is all you need. This function just makes sure that + is in place. + + """ + + # return a very basic set of requirements + return { + 'schema': NotifyGnome.protocol, + 'user': None, + 'password': None, + 'port': None, + 'host': 'localhost', + 'fullpath': None, + 'path': None, + 'url': url, + 'qsd': {}, + # Set the urgency to None so that we fall back to the default + # value. + 'urgency': None, + } diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index a37554ac..e9b1b9ea 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -33,6 +33,7 @@ from .NotifyEmail import NotifyEmail from .NotifyEmby import NotifyEmby from .NotifyFaast import NotifyFaast from .NotifyGrowl.NotifyGrowl import NotifyGrowl +from .NotifyGnome import NotifyGnome from .NotifyIFTTT import NotifyIFTTT from .NotifyJoin import NotifyJoin from .NotifyJSON import NotifyJSON @@ -65,8 +66,8 @@ from ..common import NOTIFY_TYPES __all__ = [ # Notification Services 'NotifyBoxcar', 'NotifyEmail', 'NotifyEmby', 'NotifyDiscord', - 'NotifyFaast', 'NotifyGrowl', 'NotifyIFTTT', 'NotifyJoin', 'NotifyJSON', - 'NotifyMatrix', 'NotifyMatterMost', 'NotifyProwl', 'NotifyPushalot', + 'NotifyFaast', 'NotifyGnome', 'NotifyGrowl', 'NotifyIFTTT', 'NotifyJoin', + 'NotifyJSON', 'NotifyMatterMost', 'NotifyProwl', 'NotifyPushalot', 'NotifyPushBullet', 'NotifyPushjet', 'NotifyPushover', 'NotifyRocketChat', 'NotifySlack', 'NotifyStride', 'NotifyToasty', 'NotifyTwitter', 'NotifyTelegram', 'NotifyXBMC', 'NotifyXML', 'NotifyWindows', diff --git a/test/test_gnome_plugin.py b/test/test_gnome_plugin.py new file mode 100644 index 00000000..ec8e709f --- /dev/null +++ b/test/test_gnome_plugin.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import mock +import sys +import types + +import apprise + +try: + # Python v3.4+ + from importlib import reload +except ImportError: + try: + # Python v3.0-v3.3 + from imp import reload + except ImportError: + # Python v2.7 + pass + + +def test_gnome_plugin(): + """ + API: NotifyGnome Plugin() + + """ + + # Our module base + gi_name = 'gi' + + # First we do an import without the gi library available to ensure + # we can handle cases when the library simply isn't available + + if gi_name in sys.modules: + # Test cases where the gi library exists; we want to remove it + # for the purpose of testing and capture the handling of the + # library when it is missing + del sys.modules[gi_name] + reload(sys.modules['apprise.plugins.NotifyGnome']) + + # We need to fake our gnome environment for testing purposes since + # the gi library isn't available in Travis CI + gi = types.ModuleType(gi_name) + gi.repository = types.ModuleType(gi_name + '.repository') + gi.module = types.ModuleType(gi_name + '.module') + gi.module.mock_parent = mock.Mock() + + mock_pixbuf = mock.Mock() + mock_notify = mock.Mock() + + gi.repository.GdkPixbuf = \ + types.ModuleType(gi_name + '.repository.GdkPixbuf') + gi.repository.GdkPixbuf.Pixbuf = mock_pixbuf + gi.repository.Notify = mock.Mock() + gi.repository.Notify.init.return_value = True + gi.repository.Notify.Notification = mock_notify + + # Emulate require_version function1k: + gi.require_version = mock.Mock( + name=gi_name + '.require_version') + + # Force the fake module to exist + sys.modules[gi_name] = gi + sys.modules[gi_name + '.repository'] = gi.repository + sys.modules[gi_name + '.repository.Notify'] = gi.repository.Notify + + # Notify Object + notify_obj = mock.Mock() + notify_obj.set_urgency.return_value = True + notify_obj.set_icon_from_pixbuf.return_value = True + notify_obj.set_image_from_pixbuf.return_value = True + notify_obj.show.return_value = True + mock_notify.new.return_value = notify_obj + mock_pixbuf.new_from_file.return_value = True + + # The following libraries need to be reloaded to prevent + # TypeError: super(type, obj): obj must be an instance or subtype of type + # This is better explained in this StackOverflow post: + # https://stackoverflow.com/questions/31363311/\ + # any-way-to-manually-fix-operation-of-\ + # super-after-ipython-reload-avoiding-ty + # + reload(sys.modules['apprise.plugins.NotifyGnome']) + reload(sys.modules['apprise.plugins']) + reload(sys.modules['apprise.Apprise']) + reload(sys.modules['apprise']) + + # Create our instance + obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False) + obj.duration = 0 + + # Check that it found our mocked environments + assert(obj._enabled is True) + + # test notifications + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + # test notification without a title + assert(obj.notify(title='', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + # Test our loading of our icon exception; it will still allow the + # notification to be sent + mock_pixbuf.new_from_file.side_effect = AttributeError() + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + # Undo our change + mock_pixbuf.new_from_file.side_effect = None + + # Test our exception handling during initialization + sys.modules['gi.repository.Notify']\ + .Notification.new.return_value = None + sys.modules['gi.repository.Notify']\ + .Notification.new.side_effect = AttributeError() + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False) + + # Undo our change + sys.modules['gi.repository.Notify']\ + .Notification.new.side_effect = None + + # Toggle our testing for when we can't send notifications because the + # package has been made unavailable to us + obj._enabled = False + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False) + + # Test the setting of a the urgency + apprise.plugins.NotifyGnome(urgency=0)