bazarr/libs/apprise/plugins/NotifyDBus.py

449 lines
14 KiB
Python
Raw Normal View History

2019-06-07 19:16:07 +08:00
# -*- coding: utf-8 -*-
# BSD 2-Clause License
2019-06-07 19:16:07 +08:00
#
# Apprise - Push Notification Library.
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
2019-06-07 19:16:07 +08:00
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
2019-06-07 19:16:07 +08:00
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
2019-06-07 19:16:07 +08:00
#
# 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.
2019-06-07 19:16:07 +08:00
#
# 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.
2019-06-07 19:16:07 +08:00
import sys
from .NotifyBase import NotifyBase
2019-06-07 19:16:07 +08:00
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
2019-06-07 19:16:07 +08:00
# 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
2019-06-07 19:16:07 +08:00
#
# now we try to determine which mainloop(s) we can access
#
# glib
try:
from dbus.mainloop.glib import DBusGMainLoop
LOOP_GLIB = DBusGMainLoop()
2022-10-11 09:19:24 +08:00
except ImportError: # pragma: no cover
2019-06-07 19:16:07 +08:00
# 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"]
2019-06-07 19:16:07 +08:00
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):
2019-06-07 19:16:07 +08:00
# 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
2022-10-11 09:19:24 +08:00
class DBusUrgency:
2019-06-07 19:16:07 +08:00
LOW = 0
NORMAL = 1
HIGH = 2
2022-10-11 09:19:24 +08:00
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,
}
2019-06-07 19:16:07 +08:00
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.')
}
2019-06-07 19:16:07 +08:00
# The default descriptive name associated with the Notification
service_name = _('DBus Notification')
# The services URL
service_url = 'http://www.freedesktop.org/Software/dbus/'
2019-06-07 19:16:07 +08:00
# The default protocols
2022-10-11 09:19:24 +08:00
# Python 3 keys() does not return a list object, it is its own dict_keys()
2019-06-07 19:16:07 +08:00
# 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
2022-10-11 09:19:24 +08:00
# TODO: Review after dropping support for Python 2.
2019-06-07 19:16:07 +08:00
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
2020-09-14 20:24:26 +08:00
# The number of milliseconds to keep the message present for
2019-06-07 19:16:07 +08:00
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'
2019-06-07 19:16:07 +08:00
# Define object templates
templates = (
2020-09-14 20:24:26 +08:00
'{schema}://',
2019-06-07 19:16:07 +08:00
)
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'urgency': {
'name': _('Urgency'),
'type': 'choice:int',
'values': DBUS_URGENCIES,
'default': DBusUrgency.NORMAL,
},
2022-10-11 09:19:24 +08:00
'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',
},
2019-06-07 19:16:07 +08:00
'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)
2019-06-07 19:16:07 +08:00
# 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
2022-10-11 09:19:24 +08:00
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']))
2019-06-07 19:16:07 +08:00
# Our x/y axis settings
2022-10-11 09:19:24 +08:00
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
2019-06-07 19:16:07 +08:00
# Track whether we want to add an image to the notification.
2019-06-07 19:16:07 +08:00
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 = ''
2019-06-07 19:16:07 +08:00
# acquire our dbus object
dbus_obj = session.get_object(
self.dbus_interface,
self.dbus_setting_location,
2019-06-07 19:16:07 +08:00
)
# Acquire our dbus interface
dbus_iface = Interface(
dbus_obj,
dbus_interface=self.dbus_interface,
2019-06-07 19:16:07 +08:00
)
# 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}')
2019-06-07 19:16:07 +08:00
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:
2019-06-07 19:16:07 +08:00
self.logger.warning('Failed to send DBus notification.')
self.logger.debug(f'DBus Exception: {e}')
2019-06-07 19:16:07 +08:00
return False
return True
def url(self, privacy=False, *args, **kwargs):
2019-06-07 19:16:07 +08:00
"""
Returns the URL built dynamically based on specified arguments.
"""
2020-09-14 20:24:26 +08:00
# Define any URL parameters
params = {
2019-06-07 19:16:07 +08:00
'image': 'yes' if self.include_image else 'no',
2022-10-11 09:19:24 +08:00
'urgency':
DBUS_URGENCIES[self.template_args['urgency']['default']]
if self.urgency not in DBUS_URGENCIES
else DBUS_URGENCIES[self.urgency],
2019-06-07 19:16:07 +08:00
}
2020-09-14 20:24:26 +08:00
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
2019-06-07 19:16:07 +08:00
# x in (x,y) screen coordinates
if self.x_axis:
2020-09-14 20:24:26 +08:00
params['x'] = str(self.x_axis)
2019-06-07 19:16:07 +08:00
# y in (x,y) screen coordinates
if self.y_axis:
2020-09-14 20:24:26 +08:00
params['y'] = str(self.y_axis)
2019-06-07 19:16:07 +08:00
2020-09-14 20:24:26 +08:00
return '{schema}://_/?{params}'.format(
schema=self.schema,
2020-09-14 20:24:26 +08:00
params=NotifyDBus.urlencode(params),
2019-06-07 19:16:07 +08:00
)
@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.
"""
2020-09-14 20:24:26 +08:00
results = NotifyBase.parse_url(url, verify_host=False)
2019-06-07 19:16:07 +08:00
# 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
2022-10-11 09:19:24 +08:00
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'])
2019-06-07 19:16:07 +08:00
2022-10-11 09:19:24 +08:00
if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
results['urgency'] = \
NotifyDBus.unquote(results['qsd']['urgency'])
2019-06-07 19:16:07 +08:00
# handle x,y coordinates
2022-10-11 09:19:24 +08:00
if 'x' in results['qsd'] and len(results['qsd']['x']):
results['x_axis'] = NotifyDBus.unquote(results['qsd'].get('x'))
2019-06-07 19:16:07 +08:00
2022-10-11 09:19:24 +08:00
if 'y' in results['qsd'] and len(results['qsd']['y']):
results['y_axis'] = NotifyDBus.unquote(results['qsd'].get('y'))
2019-06-07 19:16:07 +08:00
return results