SickGear/lib/apprise/plugins/NotifyDBus.py

450 lines
14 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# 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
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
# Default our global support flag
NOTIFY_DBUS_SUPPORT_ENABLED = False
# Image support is dependant on the GdkPixbuf library being available
NOTIFY_DBUS_IMAGE_SUPPORT = False
# The following are required to hook into the notifications:
NOTIFY_DBUS_INTERFACE = 'org.freedesktop.Notifications'
NOTIFY_DBUS_SETTING_LOCATION = '/org/freedesktop/Notifications'
# Initialize our mainloops
LOOP_GLIB = None
LOOP_QT = None
try:
# dbus essentials
from dbus import SessionBus
from dbus import Interface
from dbus import Byte
from dbus import ByteArray
from dbus import DBusException
#
# now we try to determine which mainloop(s) we can access
#
# glib
try:
from dbus.mainloop.glib import DBusGMainLoop
LOOP_GLIB = DBusGMainLoop()
except ImportError:
# No problem
pass
# qt
try:
from dbus.mainloop.qt import DBusQtMainLoop
LOOP_QT = DBusQtMainLoop(set_as_default=True)
except ImportError:
# No problem
pass
# We're good as long as at least one
NOTIFY_DBUS_SUPPORT_ENABLED = (
LOOP_GLIB is not None or LOOP_QT is not None)
try:
# The following is required for Image/Icon loading only
import gi
gi.require_version('GdkPixbuf', '2.0')
# noinspection PyUnresolvedReferences
from gi.repository import GdkPixbuf
NOTIFY_DBUS_IMAGE_SUPPORT = True
except (ImportError, ValueError, AttributeError):
# No problem; this will get caught in outer try/catch
# A ValueError will get thrown upon calling gi.require_version() if
# GDK/GTK isn't installed on the system but gi is.
pass
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
# Define our supported protocols and the loop to assign them.
# The key to value pairs are the actual supported schema's matched
# up with the Main Loop they should reference when accessed.
MAINLOOP_MAP = {
'qt': LOOP_QT,
'kde': LOOP_QT,
'glib': LOOP_GLIB,
'dbus': LOOP_QT if LOOP_QT else LOOP_GLIB,
}
# Urgencies
class DBusUrgency(object):
LOW = 0
NORMAL = 1
HIGH = 2
# Define our urgency levels
DBUS_URGENCIES = (
DBusUrgency.LOW,
DBusUrgency.NORMAL,
DBusUrgency.HIGH,
)
class NotifyDBus(NotifyBase):
"""
A wrapper for local DBus/Qt Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'DBus Notification'
# The default protocols
# Python 3 keys() does not return a list object, it's it's own dict_keys()
# object if we were to reference, we wouldn't be backwards compatible with
# Python v2. So converting the result set back into a list makes us
# compatible
protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_dbus'
# No throttling required for DBus queries
request_rate_per_sec = 0
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
# The number of seconds to keep the message present for
message_timeout_ms = 13000
# Limit results to just the first 10 line otherwise there is just to much
# content to display
body_max_line_count = 10
# 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_glib_plugin.py, please
# let me know! :)
_enabled = NOTIFY_DBUS_SUPPORT_ENABLED
# Define object templates
templates = (
'{schema}://_/',
)
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'urgency': {
'name': _('Urgency'),
'type': 'choice:int',
'values': DBUS_URGENCIES,
'default': DBusUrgency.NORMAL,
},
'x': {
'name': _('X-Axis'),
'type': 'int',
'min': 0,
'map_to': 'x_axis',
},
'y': {
'name': _('Y-Axis'),
'type': 'int',
'min': 0,
'map_to': 'y_axis',
},
'image': {
'name': _('Include Image'),
'type': 'bool',
'default': True,
'map_to': 'include_image',
},
})
def __init__(self, urgency=None, x_axis=None, y_axis=None,
include_image=True, **kwargs):
"""
Initialize DBus Object
"""
super(NotifyDBus, self).__init__(**kwargs)
# Track our notifications
self.registry = {}
# Store our schema; default to dbus
self.schema = kwargs.get('schema', 'dbus')
if self.schema not in MAINLOOP_MAP:
msg = 'The schema specified ({}) is not supported.' \
.format(self.schema)
self.logger.warning(msg)
raise TypeError(msg)
# The urgency of the message
if urgency not in DBUS_URGENCIES:
self.urgency = DBusUrgency.NORMAL
else:
self.urgency = urgency
# Our x/y axis settings
self.x_axis = x_axis if isinstance(x_axis, int) else None
self.y_axis = y_axis if isinstance(y_axis, int) else None
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform DBus Notification
"""
if not self._enabled or MAINLOOP_MAP[self.schema] is None:
self.logger.warning(
"{} notifications could not be loaded.".format(self.schema))
return False
# Acquire our session
try:
session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])
except DBusException:
# Handle exception
self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception')
return False
# If there is no title, but there is a body, swap the two to get rid
# of the weird whitespace
if not title:
title = body
body = ''
# acquire our dbus object
dbus_obj = session.get_object(
NOTIFY_DBUS_INTERFACE,
NOTIFY_DBUS_SETTING_LOCATION,
)
# Acquire our dbus interface
dbus_iface = Interface(
dbus_obj,
dbus_interface=NOTIFY_DBUS_INTERFACE,
)
# image path
icon_path = None if not self.include_image \
else self.image_path(notify_type, extension='.ico')
# Our meta payload
meta_payload = {
"urgency": Byte(self.urgency)
}
if not (self.x_axis is None and self.y_axis is None):
# Set x/y access if these were set
meta_payload['x'] = self.x_axis
meta_payload['y'] = self.y_axis
if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path:
try:
# Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
# Associate our image to our notification
meta_payload['icon_data'] = (
image.get_width(),
image.get_height(),
image.get_rowstride(),
image.get_has_alpha(),
image.get_bits_per_sample(),
image.get_n_channels(),
ByteArray(image.get_pixels())
)
except Exception as e:
self.logger.warning(
"Could not load Gnome notification icon ({}): {}"
.format(icon_path, e))
try:
# Always call throttle() before any remote execution is made
self.throttle()
dbus_iface.Notify(
# Application Identifier
self.app_id,
# Message ID (0 = New Message)
0,
# Icon (str) - not used
'',
# Title
str(title),
# Body
str(body),
# Actions
list(),
# Meta
meta_payload,
# Message Timeout
self.message_timeout_ms,
)
self.logger.info('Sent DBus notification.')
except Exception:
self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception')
return False
return True
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
_map = {
DBusUrgency.LOW: 'low',
DBusUrgency.NORMAL: 'normal',
DBusUrgency.HIGH: 'high',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency],
'verify': 'yes' if self.verify_certificate else 'no',
}
# x in (x,y) screen coordinates
if self.x_axis:
args['x'] = str(self.x_axis)
# y in (x,y) screen coordinates
if self.y_axis:
args['y'] = str(self.y_axis)
return '{schema}://_/?{args}'.format(
schema=self.schema,
args=NotifyDBus.urlencode(args),
)
@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.
"""
schema = GET_SCHEMA_RE.match(url)
if schema is None:
# Content is simply not parseable
return None
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': schema.group('schema').lower(),
'user': None,
'password': None,
'port': None,
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))
# DBus supports urgency, but we we also support the keyword priority
# so that it is consistent with some of the other plugins
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
if urgency and len(urgency):
_map = {
'0': DBusUrgency.LOW,
'l': DBusUrgency.LOW,
'n': DBusUrgency.NORMAL,
'1': DBusUrgency.NORMAL,
'h': DBusUrgency.HIGH,
'2': DBusUrgency.HIGH,
}
try:
# Attempt to index/retrieve our urgency
results['urgency'] = _map[urgency[0].lower()]
except KeyError:
# No priority was set
pass
# handle x,y coordinates
try:
results['x_axis'] = int(results['qsd'].get('x'))
except (TypeError, ValueError):
# No x was set
pass
try:
results['y_axis'] = int(results['qsd'].get('y'))
except (TypeError, ValueError):
# No y was set
pass
return results