aboutsummaryrefslogtreecommitdiffhomepage
path: root/libs/apprise/plugins/dbus.py
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2024-05-11 23:22:55 -0400
committermorpheus65535 <[email protected]>2024-05-11 23:22:55 -0400
commit86d34039a35387e33663f14b30a65cc1165b4fc7 (patch)
treef705dda4032cf885d7aac8afcc5ddfa7c97ed11b /libs/apprise/plugins/dbus.py
parent006ee0f63ac39dc1e73c761a161aacfc6d62b380 (diff)
downloadbazarr-86d34039a35387e33663f14b30a65cc1165b4fc7.tar.gz
bazarr-86d34039a35387e33663f14b30a65cc1165b4fc7.zip
Updated apprise to 1.8.0v1.4.3-beta.36
Diffstat (limited to 'libs/apprise/plugins/dbus.py')
-rw-r--r--libs/apprise/plugins/dbus.py448
1 files changed, 448 insertions, 0 deletions
diff --git a/libs/apprise/plugins/dbus.py b/libs/apprise/plugins/dbus.py
new file mode 100644
index 000000000..f2361fd62
--- /dev/null
+++ b/libs/apprise/plugins/dbus.py
@@ -0,0 +1,448 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron <[email protected]>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import sys
+from .base import NotifyBase
+from ..common import NotifyImageSize
+from ..common import NotifyType
+from ..utils import parse_bool
+from ..locale 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
+
+# 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: # pragma: no cover
+ # 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)
+
+ # ImportError: When using gi.repository you must not import static modules
+ # like "gobject". Please change all occurrences of "import gobject" to
+ # "from gi.repository import GObject".
+ # See: https://bugzilla.gnome.org/show_bug.cgi?id=709183
+ if "gobject" in sys.modules: # pragma: no cover
+ del sys.modules["gobject"]
+
+ try:
+ # The following is required for Image/Icon loading only
+ import gi
+ gi.require_version('GdkPixbuf', '2.0')
+ 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:
+ LOW = 0
+ NORMAL = 1
+ HIGH = 2
+
+
+DBUS_URGENCIES = {
+ # Note: This also acts as a reverse lookup mapping
+ DBusUrgency.LOW: 'low',
+ DBusUrgency.NORMAL: 'normal',
+ DBusUrgency.HIGH: 'high',
+}
+
+DBUS_URGENCY_MAP = {
+ # Maps against string 'low'
+ 'l': DBusUrgency.LOW,
+ # Maps against string 'moderate'
+ 'm': DBusUrgency.LOW,
+ # Maps against string 'normal'
+ 'n': DBusUrgency.NORMAL,
+ # Maps against string 'high'
+ 'h': DBusUrgency.HIGH,
+ # Maps against string 'emergency'
+ 'e': DBusUrgency.HIGH,
+
+ # Entries to additionally support (so more like DBus's API)
+ '0': DBusUrgency.LOW,
+ '1': DBusUrgency.NORMAL,
+ '2': DBusUrgency.HIGH,
+}
+
+
+class NotifyDBus(NotifyBase):
+ """
+ A wrapper for local DBus/Qt Notifications
+ """
+
+ # Set our global enabled flag
+ enabled = NOTIFY_DBUS_SUPPORT_ENABLED
+
+ requirements = {
+ # Define our required packaging in order to work
+ 'details': _('libdbus-1.so.x must be installed.')
+ }
+
+ # The default descriptive name associated with the Notification
+ service_name = _('DBus Notification')
+
+ # The services URL
+ service_url = 'http://www.freedesktop.org/Software/dbus/'
+
+ # The default protocols
+ # Python 3 keys() does not return a list object, it is its 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
+ # TODO: Review after dropping support for Python 2.
+ 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 milliseconds 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
+
+ # The following are required to hook into the notifications:
+ dbus_interface = 'org.freedesktop.Notifications'
+ dbus_setting_location = '/org/freedesktop/Notifications'
+
+ # 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,
+ },
+ 'priority': {
+ # Apprise uses 'priority' everywhere; it's just a nice consistent
+ # feel to be able to use it here as well. Just map the
+ # value back to 'priority'
+ 'alias_of': 'urgency',
+ },
+ '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().__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
+ self.urgency = int(
+ NotifyDBus.template_args['urgency']['default']
+ if urgency is None else
+ next((
+ v for k, v in DBUS_URGENCY_MAP.items()
+ if str(urgency).lower().startswith(k)),
+ NotifyDBus.template_args['urgency']['default']))
+
+ # Our x/y axis settings
+ if x_axis or y_axis:
+ try:
+ self.x_axis = int(x_axis)
+ self.y_axis = int(y_axis)
+
+ except (TypeError, ValueError):
+ # Invalid x/y values specified
+ msg = 'The x,y coordinates specified ({},{}) are invalid.'\
+ .format(x_axis, y_axis)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+ else:
+ self.x_axis = None
+ self.y_axis = None
+
+ # Track whether we want to add an image to the notification.
+ self.include_image = include_image
+
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
+ """
+ Perform DBus Notification
+ """
+ # Acquire our session
+ try:
+ session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])
+
+ except DBusException as e:
+ # Handle exception
+ self.logger.warning('Failed to send DBus notification.')
+ self.logger.debug(f'DBus Exception: {e}')
+ 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(
+ self.dbus_interface,
+ self.dbus_setting_location,
+ )
+
+ # Acquire our dbus interface
+ dbus_iface = Interface(
+ dbus_obj,
+ dbus_interface=self.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 notification icon (%s).", icon_path)
+ self.logger.debug(f'DBus Exception: {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 as e:
+ self.logger.warning('Failed to send DBus notification.')
+ self.logger.debug(f'DBus Exception: {e}')
+ return False
+
+ return True
+
+ def url(self, privacy=False, *args, **kwargs):
+ """
+ Returns the URL built dynamically based on specified arguments.
+ """
+
+ # Define any URL parameters
+ params = {
+ 'image': 'yes' if self.include_image else 'no',
+ 'urgency':
+ DBUS_URGENCIES[self.template_args['urgency']['default']]
+ if self.urgency not in DBUS_URGENCIES
+ else DBUS_URGENCIES[self.urgency],
+ }
+
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ # x in (x,y) screen coordinates
+ if self.x_axis:
+ params['x'] = str(self.x_axis)
+
+ # y in (x,y) screen coordinates
+ if self.y_axis:
+ params['y'] = str(self.y_axis)
+
+ return '{schema}://_/?{params}'.format(
+ schema=self.schema,
+ params=NotifyDBus.urlencode(params),
+ )
+
+ @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.
+
+ """
+
+ results = NotifyBase.parse_url(url, verify_host=False)
+
+ # 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
+ if 'priority' in results['qsd'] and len(results['qsd']['priority']):
+ # We intentionally store the priority in the urgency section
+ results['urgency'] = \
+ NotifyDBus.unquote(results['qsd']['priority'])
+
+ if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
+ results['urgency'] = \
+ NotifyDBus.unquote(results['qsd']['urgency'])
+
+ # handle x,y coordinates
+ if 'x' in results['qsd'] and len(results['qsd']['x']):
+ results['x_axis'] = NotifyDBus.unquote(results['qsd'].get('x'))
+
+ if 'y' in results['qsd'] and len(results['qsd']['y']):
+ results['y_axis'] = NotifyDBus.unquote(results['qsd'].get('y'))
+
+ return results