diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
commit | 2e2851dc13d73352530dd4495c7e05603b2e520d (patch) | |
tree | 622b9cd8e5d32091c9aa9e4937b533975a40356c /deluge/plugins/Notifications/deluge_notifications | |
parent | Initial commit. (diff) | |
download | deluge-2e2851dc13d73352530dd4495c7e05603b2e520d.tar.xz deluge-2e2851dc13d73352530dd4495c7e05603b2e520d.zip |
Adding upstream version 2.1.2~dev0+20240219.upstream/2.1.2_dev0+20240219upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/plugins/Notifications/deluge_notifications')
8 files changed, 2401 insertions, 0 deletions
diff --git a/deluge/plugins/Notifications/deluge_notifications/__init__.py b/deluge/plugins/Notifications/deluge_notifications/__init__.py new file mode 100644 index 0000000..d52b48d --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/__init__.py @@ -0,0 +1,38 @@ +# +# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from deluge.plugins.init import PluginInitBase + + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from .core import Core as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtkui import GtkUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .webui import WebUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) diff --git a/deluge/plugins/Notifications/deluge_notifications/common.py b/deluge/plugins/Notifications/deluge_notifications/common.py new file mode 100644 index 0000000..9993f5c --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/common.py @@ -0,0 +1,114 @@ +# +# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import os.path + +from pkg_resources import resource_filename +from twisted.internet import defer + +from deluge import component +from deluge.event import known_events + +log = logging.getLogger(__name__) + + +def get_resource(filename): + return resource_filename(__package__, os.path.join('data', filename)) + + +class CustomNotifications: + def __init__(self, plugin_name=None): + self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}} + + def enable(self): + pass + + def disable(self): + for kind in self.custom_notifications: + for eventtype in list(self.custom_notifications[kind]): + wrapper, handler = self.custom_notifications[kind][eventtype] + self._deregister_custom_provider(kind, eventtype) + + def _handle_custom_providers(self, kind, eventtype, *args, **kwargs): + log.debug( + 'Calling CORE custom %s providers for %s: %s %s', + kind, + eventtype, + args, + kwargs, + ) + if eventtype in self.config['subscriptions'][kind]: + wrapper, handler = self.custom_notifications[kind][eventtype] + log.debug('Found handler for kind %s: %s', kind, handler) + custom_notif_func = getattr(self, 'handle_custom_%s_notification' % kind) + d = defer.maybeDeferred(handler, *args, **kwargs) + d.addCallback(custom_notif_func, eventtype) + d.addCallback(self._on_notify_sucess, kind) + d.addErrback(self._on_notify_failure, kind) + return d + + def _register_custom_provider(self, kind, eventtype, handler): + if not self._handled_eventtype(eventtype, handler): + return defer.succeed('Event not handled') + if eventtype not in self.custom_notifications: + + def wrapper(*args, **kwargs): + return self._handle_custom_providers(kind, eventtype, *args, **kwargs) + + self.custom_notifications[kind][eventtype] = (wrapper, handler) + else: + wrapper, handler = self.custom_notifications[kind][eventtype] + try: + component.get('EventManager').register_event_handler(eventtype, wrapper) + except KeyError: + from deluge.ui.client import client + + client.register_event_handler(eventtype, wrapper) + + def _deregister_custom_provider(self, kind, eventtype): + try: + wrapper, handler = self.custom_notifications[kind][eventtype] + try: + component.get('EventManager').deregister_event_handler( + eventtype, wrapper + ) + except KeyError: + from deluge.ui.client import client + + client.deregister_event_handler(eventtype, wrapper) + self.custom_notifications[kind].pop(eventtype) + except KeyError: + pass + + def _handled_eventtype(self, eventtype, handler): + if eventtype not in known_events: + log.error('The event "%s" is not known', eventtype) + return False + if known_events[eventtype].__module__.startswith('deluge.event'): + if handler.__self__ is self: + return True + log.error( + 'You cannot register custom notification providers ' + 'for built-in event types.' + ) + return False + return True + + def _on_notify_sucess(self, result, kind): + log.debug('Notification success using %s: %s', kind, result) + return result + + def _on_notify_failure(self, failure, kind): + log.debug('Notification failure using %s: %s', kind, failure) + return failure diff --git a/deluge/plugins/Notifications/deluge_notifications/core.py b/deluge/plugins/Notifications/deluge_notifications/core.py new file mode 100644 index 0000000..aa200f9 --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/core.py @@ -0,0 +1,228 @@ +# +# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import smtplib +from email.utils import formatdate + +from twisted.internet import defer, threads + +import deluge.configmanager +from deluge import component +from deluge.core.rpcserver import export +from deluge.event import known_events +from deluge.plugins.pluginbase import CorePluginBase + +from .common import CustomNotifications + +log = logging.getLogger(__name__) + +DEFAULT_PREFS = { + 'smtp_enabled': False, + 'smtp_host': '', + 'smtp_port': 25, + 'smtp_user': '', + 'smtp_pass': '', + 'smtp_from': '', + 'smtp_tls': False, # SSL or TLS + 'smtp_recipients': [], + # Subscriptions + 'subscriptions': {'email': []}, +} + + +class CoreNotifications(CustomNotifications): + def __init__(self, plugin_name=None): + CustomNotifications.__init__(self, plugin_name) + + def enable(self): + CustomNotifications.enable(self) + self.register_custom_email_notification( + 'TorrentFinishedEvent', self._on_torrent_finished_event + ) + + def disable(self): + self.deregister_custom_email_notification('TorrentFinishedEvent') + CustomNotifications.disable(self) + + def register_custom_email_notification(self, eventtype, handler): + """This is used to register email notifications for custom event types. + + :param event: str, the event name + :param handler: function, to be called when `:param:event` is emitted + + Your handler should return a tuple of (email_subject, email_contents). + """ + self._register_custom_provider('email', eventtype, handler) + + def deregister_custom_email_notification(self, eventtype): + self._deregister_custom_provider('email', eventtype) + + def handle_custom_email_notification(self, result, eventtype): + if not self.config['smtp_enabled']: + return defer.succeed('SMTP notification not enabled.') + subject, message = result + log.debug( + 'Spawning new thread to send email with subject: %s: %s', subject, message + ) + # Spawn thread because we don't want Deluge to lock up while we send the + # email. + return threads.deferToThread(self._notify_email, subject, message) + + def get_handled_events(self): + handled_events = [] + for evt in sorted(known_events): + if known_events[evt].__module__.startswith('deluge.event'): + if evt not in ('TorrentFinishedEvent',): + # Skip all un-handled built-in events + continue + classdoc = known_events[evt].__doc__.strip() + handled_events.append((evt, classdoc)) + log.debug('Handled Notification Events: %s', handled_events) + return handled_events + + def _notify_email(self, subject='', message=''): + log.debug('Email prepared') + to_addrs = self.config['smtp_recipients'] + to_addrs_str = ', '.join(self.config['smtp_recipients']) + headers_dict = { + 'smtp_from': self.config['smtp_from'], + 'subject': subject, + 'smtp_recipients': to_addrs_str, + 'date': formatdate(), + } + headers = ( + """\ +From: %(smtp_from)s +To: %(smtp_recipients)s +Subject: %(subject)s +Date: %(date)s + + +""" + % headers_dict + ) + + message = '\r\n'.join((headers + message).splitlines()) + + try: + server = smtplib.SMTP( + self.config['smtp_host'], self.config['smtp_port'], timeout=60 + ) + except Exception as ex: + err_msg = _('There was an error sending the notification email: %s') % ex + log.error(err_msg) + return ex + + security_enabled = self.config['smtp_tls'] + + if security_enabled: + server.ehlo() + if 'starttls' not in server.esmtp_features: + log.warning('TLS/SSL enabled but server does not support it') + else: + server.starttls() + server.ehlo() + + if self.config['smtp_user'] and self.config['smtp_pass']: + try: + server.login(self.config['smtp_user'], self.config['smtp_pass']) + except smtplib.SMTPHeloError as ex: + err_msg = _('Server did not reply properly to HELO greeting: %s') % ex + log.error(err_msg) + return ex + except smtplib.SMTPAuthenticationError as ex: + err_msg = _('Server refused username/password combination: %s') % ex + log.error(err_msg) + return ex + + try: + try: + server.sendmail(self.config['smtp_from'], to_addrs, message.encode()) + except smtplib.SMTPException as ex: + err_msg = ( + _('There was an error sending the notification email: %s') % ex + ) + log.error(err_msg) + return ex + finally: + if security_enabled: + # avoid false failure detection when the server closes + # the SMTP connection with TLS enabled + import socket + + try: + server.quit() + except socket.sslerror: + pass + else: + server.quit() + return _('Notification email sent.') + + def _on_torrent_finished_event(self, torrent_id): + log.debug('Handler for TorrentFinishedEvent called for CORE') + torrent = component.get('TorrentManager')[torrent_id] + torrent_status = torrent.get_status(['name', 'num_files']) + # Email + subject = _('Finished Torrent "%(name)s"') % torrent_status + message = ( + _( + 'This email is to inform you that Deluge has finished ' + 'downloading "%(name)s", which includes %(num_files)i files.' + '\nTo stop receiving these alerts, simply turn off email ' + "notification in Deluge's preferences.\n\n" + 'Thank you,\nDeluge.' + ) + % torrent_status + ) + return subject, message + + # d = defer.maybeDeferred(self.handle_custom_email_notification, + # [subject, message], + # 'TorrentFinishedEvent') + # d.addCallback(self._on_notify_sucess, 'email') + # d.addErrback(self._on_notify_failure, 'email') + # return d + + +class Core(CorePluginBase, CoreNotifications): + def __init__(self, plugin_name): + CorePluginBase.__init__(self, plugin_name) + CoreNotifications.__init__(self) + + def enable(self): + CoreNotifications.enable(self) + self.config = deluge.configmanager.ConfigManager( + 'notifications-core.conf', DEFAULT_PREFS + ) + log.debug('ENABLING CORE NOTIFICATIONS') + + def disable(self): + log.debug('DISABLING CORE NOTIFICATIONS') + CoreNotifications.disable(self) + + @export + def set_config(self, config): + """Sets the config dictionary.""" + for key in config: + self.config[key] = config[key] + self.config.save() + + @export + def get_config(self): + """Returns the config dictionary.""" + return self.config.config + + @export + def get_handled_events(self): + return CoreNotifications.get_handled_events(self) diff --git a/deluge/plugins/Notifications/deluge_notifications/data/config.ui b/deluge/plugins/Notifications/deluge_notifications/data/config.ui new file mode 100644 index 0000000..399cc9e --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/data/config.ui @@ -0,0 +1,641 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">1</property> + <property name="upper">65535</property> + <property name="value">25</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkWindow" id="window"> + <property name="can_focus">False</property> + <child type="titlebar"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="prefs_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <child> + <object class="GtkNotebook" id="notebook1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_border">False</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="margin_left">9</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <property name="right_padding">10</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="blink_enabled"> + <property name="label" translatable="yes">Tray icon blinks enabled</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="popup_enabled"> + <property name="label" translatable="yes">Popups enabled</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="sound_enabled"> + <property name="label" translatable="yes">Sound enabled</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_sound_enabled_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFileChooserButton" id="sound_path"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="create_folders">False</property> + <signal name="update-preview" handler="on_sound_path_update_preview" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">2</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_top">5</property> + <property name="xpad">5</property> + <property name="label" translatable="yes"><b>UI Notifications</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="margin_left">10</property> + <property name="margin_top">7</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <property name="right_padding">10</property> + <child> + <object class="GtkTable" id="prefs_table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">7</property> + <property name="n_columns">4</property> + <property name="column_spacing">2</property> + <property name="row_spacing">2</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Hostname:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="smtp_host"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Port:</property> + <property name="justify">right</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="smtp_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_length">5</property> + <property name="width_chars">5</property> + <property name="adjustment">adjustment1</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">4</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Username:</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="smtp_user"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Password:</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="smtp_pass"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="visibility">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="bottom_padding">10</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="smtp_recipients"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <property name="enable_grid_lines">horizontal</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVButtonBox" id="vbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="add_button"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_add_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="delete_button"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_delete_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">3</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_bottom">3</property> + <property name="ypad">0</property> + <property name="label" translatable="yes"><b>Recipients</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="right_attach">4</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="smtp_tls"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Server requires TLS/SSL</property> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">From:</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="smtp_from"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="smtp_enabled"> + <property name="label" translatable="yes">Enabled</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_enabled_toggled" swapped="no"/> + </object> + <packing> + <property name="right_attach">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="xpad">5</property> + <property name="label" translatable="yes"><b>Email Notifications</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="settings_page_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Settings</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">15</property> + <property name="margin_right">10</property> + <property name="margin_bottom">10</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="subscriptions_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="enable_grid_lines">horizontal</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">This configuration does not mean that you'll actually receive notifications for all these events.</property> + <property name="justify">fill</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">2</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="subscriptions_page_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Subscriptions</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="sounds_page"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">15</property> + <property name="margin_right">10</property> + <property name="margin_bottom">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="sounds_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="sounds_revert_button"> + <property name="label">gtk-revert-to-saved</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_sounds_revert_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="sounds_edit_button"> + <property name="label">gtk-edit</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_sounds_edit_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="sounds_page_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Sound Customization</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Notifications/deluge_notifications/data/notifications.js b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js new file mode 100644 index 0000000..4b87e55 --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js @@ -0,0 +1,522 @@ +/** + * notifications.js + * + * Copyright (c) Omar Alvarez 2014 <omar.alvarez@udc.es> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + * + */ + +Ext.ns('Deluge.ux.preferences'); + +/** + * @class Deluge.ux.preferences.NotificationsPage + * @extends Ext.Panel + */ +Deluge.ux.preferences.NotificationsPage = Ext.extend(Ext.Panel, { + title: _('Notifications'), + header: false, + layout: 'fit', + border: false, + + initComponent: function () { + Deluge.ux.preferences.NotificationsPage.superclass.initComponent.call( + this + ); + + this.emailNotiFset = new Ext.form.FieldSet({ + xtype: 'fieldset', + border: false, + title: _('Email Notifications'), + autoHeight: true, + defaultType: 'textfield', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + width: '85%', + labelWidth: 1, + }); + + this.chkEnableEmail = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'enable_email', + xtype: 'checkbox', + boxLabel: _('Enabled'), + listeners: { + check: function (object, checked) { + this.setSmtpDisabled(!checked); + }, + scope: this, + }, + }); + + this.hBoxHost = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'host', + xtype: 'container', + layout: 'hbox', + disabled: true, + items: [ + { + xtype: 'label', + text: _('Hostname:'), + margins: '6 0 0 6', + }, + { + xtype: 'textfield', + margins: '2 0 0 4', + }, + ], + }); + + this.hBoxPort = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'port', + xtype: 'container', + layout: 'hbox', + disabled: true, + items: [ + { + xtype: 'label', + text: _('Port:'), + margins: '6 0 0 6', + }, + { + xtype: 'spinnerfield', + margins: '2 0 0 34', + width: 64, + decimalPrecision: 0, + minValue: 0, + maxValue: 65535, + }, + ], + }); + + this.hBoxUser = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'username', + xtype: 'container', + layout: 'hbox', + disabled: true, + items: [ + { + xtype: 'label', + text: _('Username:'), + margins: '6 0 0 6', + }, + { + xtype: 'textfield', + margins: '2 0 0 3', + }, + ], + }); + + this.hBoxPassword = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'password', + xtype: 'container', + layout: 'hbox', + disabled: true, + items: [ + { + xtype: 'label', + text: _('Password:'), + margins: '6 0 0 6', + }, + { + xtype: 'textfield', + inputType: 'password', + margins: '2 0 0 5', + }, + ], + }); + + this.hBoxFrom = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'from', + xtype: 'container', + layout: 'hbox', + disabled: true, + items: [ + { + xtype: 'label', + text: _('From:'), + margins: '6 0 0 6', + }, + { + xtype: 'textfield', + margins: '2 0 0 28', + }, + ], + }); + + this.chkTLS = this.emailNotiFset.add({ + fieldLabel: '', + labelSeparator: '', + name: 'enable_tls_ssl', + xtype: 'checkbox', + disabled: true, + boxLabel: _('Server requires TLS/SSL'), + }); + + this.recipientsFset = new Ext.form.FieldSet({ + xtype: 'fieldset', + border: false, + title: _('Recipients'), + autoHeight: true, + defaultType: 'editorgrid', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: true, + items: [ + { + fieldLabel: '', + name: 'recipients', + margins: '2 0 5 5', + height: 130, + hideHeaders: true, + width: 260, + disabled: true, + autoExpandColumn: 'recipient', + bbar: { + items: [ + { + text: _('Add'), + iconCls: 'icon-add', + handler: this.onAddClick, + scope: this, + }, + { + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onRemoveClick, + scope: this, + }, + ], + }, + viewConfig: { + emptyText: _('Add an recipient...'), + deferEmptyText: false, + }, + colModel: new Ext.grid.ColumnModel({ + columns: [ + { + id: 'recipient', + header: _('Recipient'), + dataIndex: 'recipient', + sortable: true, + hideable: false, + editable: true, + editor: { + xtype: 'textfield', + }, + }, + ], + }), + selModel: new Ext.grid.RowSelectionModel({ + singleSelect: false, + moveEditorOnEnter: false, + }), + store: new Ext.data.ArrayStore({ + autoDestroy: true, + fields: [{ name: 'recipient' }], + }), + listeners: { + afteredit: function (e) { + e.record.commit(); + }, + }, + setEmptyText: function (text) { + if (this.viewReady) { + this.getView().emptyText = text; + this.getView().refresh(); + } else { + Ext.apply(this.viewConfig, { emptyText: text }); + } + }, + loadData: function (data) { + this.getStore().loadData(data); + if (this.viewReady) { + this.getView().updateHeaders(); + } + }, + }, + ], + }); + + this.edGridSubs = new Ext.grid.EditorGridPanel({ + xtype: 'editorgrid', + autoHeight: true, + autoExpandColumn: 'event', + viewConfig: { + emptyText: _('Loading events...'), + deferEmptyText: false, + }, + colModel: new Ext.grid.ColumnModel({ + defaults: { + renderer: function ( + value, + meta, + record, + rowIndex, + colIndex, + store + ) { + if (Ext.isNumber(value) && parseInt(value) !== value) { + return value.toFixed(6); + } else if (Ext.isBoolean(value)) { + return ( + '<div class="x-grid3-check-col' + + (value ? '-on' : '') + + '" style="width: 20px;"> </div>' + ); + } + return value; + }, + }, + columns: [ + { + id: 'event', + header: 'Event', + dataIndex: 'event', + sortable: true, + hideable: false, + }, + { + id: 'email', + header: _('Email'), + dataIndex: 'email', + sortable: true, + hideable: false, + menuDisabled: true, + width: 40, + }, + ], + }), + store: new Ext.data.ArrayStore({ + autoDestroy: true, + fields: [ + { + name: 'event', + }, + { + name: 'email', + }, + ], + }), + listeners: { + cellclick: function (grid, rowIndex, colIndex, e) { + var record = grid.getStore().getAt(rowIndex); + var field = grid.getColumnModel().getDataIndex(colIndex); + var value = record.get(field); + + if (colIndex == 1) { + if (Ext.isBoolean(value)) { + record.set(field, !value); + record.commit(); + } + } + }, + beforeedit: function (e) { + if (Ext.isBoolean(e.value)) { + return false; + } + + return e.record.get('enabled'); + }, + afteredit: function (e) { + e.record.commit(); + }, + }, + setEmptyText: function (text) { + if (this.viewReady) { + this.getView().emptyText = text; + this.getView().refresh(); + } else { + Ext.apply(this.viewConfig, { emptyText: text }); + } + }, + setSub: function (eventName) { + var store = this.getStore(); + var index = store.find('event', eventName); + store.getAt(index).set('email', true); + store.getAt(index).commit(); + }, + loadData: function (data) { + this.getStore().loadData(data); + if (this.viewReady) { + this.getView().updateHeaders(); + } + }, + }); + + this.tabPanSettings = this.add({ + xtype: 'tabpanel', + activeTab: 0, + items: [ + { + title: _('Settings'), + items: [this.emailNotiFset, this.recipientsFset], + autoScroll: true, + }, + { + title: _('Subscriptions'), + items: this.edGridSubs, + }, + ], + }); + + this.on('show', this.updateConfig, this); + }, + + updateConfig: function () { + deluge.client.notifications.get_handled_events({ + success: function (events) { + var data = []; + var keys = Ext.keys(events); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + data.push([events[key][0], false]); + } + this.edGridSubs.loadData(data); + }, + scope: this, + }); + deluge.client.notifications.get_config({ + success: function (config) { + this.chkEnableEmail.setValue(config['smtp_enabled']); + this.setSmtpDisabled(!config['smtp_enabled']); + + this.hBoxHost.getComponent(1).setValue(config['smtp_host']); + this.hBoxPort.getComponent(1).setValue(config['smtp_port']); + this.hBoxUser.getComponent(1).setValue(config['smtp_user']); + this.hBoxPassword.getComponent(1).setValue(config['smtp_pass']); + this.hBoxFrom.getComponent(1).setValue(config['smtp_from']); + this.chkTLS.setValue(config['smtp_tls']); + + var data = []; + var keys = Ext.keys(config['smtp_recipients']); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + data.push([config['smtp_recipients'][key]]); + } + this.recipientsFset.getComponent(0).loadData(data); + + data = []; + keys = Ext.keys(config['subscriptions']['email']); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + this.edGridSubs.setSub( + config['subscriptions']['email'][key] + ); + } + }, + scope: this, + }); + }, + + onApply: function () { + var config = {}; + + config['smtp_enabled'] = this.chkEnableEmail.getValue(); + config['smtp_host'] = this.hBoxHost.getComponent(1).getValue(); + config['smtp_port'] = Number(this.hBoxPort.getComponent(1).getValue()); + config['smtp_user'] = this.hBoxUser.getComponent(1).getValue(); + config['smtp_pass'] = this.hBoxPassword.getComponent(1).getValue(); + config['smtp_from'] = this.hBoxFrom.getComponent(1).getValue(); + config['smtp_tls'] = this.chkTLS.getValue(); + + var recipientsList = []; + var store = this.recipientsFset.getComponent(0).getStore(); + + for (var i = 0; i < store.getCount(); i++) { + var record = store.getAt(i); + var recipient = record.get('recipient'); + recipientsList.push(recipient); + } + + config['smtp_recipients'] = recipientsList; + + var subscriptions = {}; + var eventList = []; + store = this.edGridSubs.getStore(); + + for (var i = 0; i < store.getCount(); i++) { + var record = store.getAt(i); + var ev = record.get('event'); + var email = record.get('email'); + if (email) { + eventList.push(ev); + } + } + + subscriptions['email'] = eventList; + config['subscriptions'] = subscriptions; + + deluge.client.notifications.set_config(config); + }, + + onOk: function () { + this.onApply(); + }, + + onAddClick: function () { + var store = this.recipientsFset.getComponent(0).getStore(); + var Recipient = store.recordType; + var i = new Recipient({ + recipient: '', + }); + this.recipientsFset.getComponent(0).stopEditing(); + store.insert(0, i); + this.recipientsFset.getComponent(0).startEditing(0, 0); + }, + + onRemoveClick: function () { + var selections = this.recipientsFset + .getComponent(0) + .getSelectionModel() + .getSelections(); + var store = this.recipientsFset.getComponent(0).getStore(); + + this.recipientsFset.getComponent(0).stopEditing(); + for (var i = 0; i < selections.length; i++) store.remove(selections[i]); + store.commitChanges(); + }, + + setSmtpDisabled: function (disable) { + this.hBoxHost.setDisabled(disable); + this.hBoxPort.setDisabled(disable); + this.hBoxUser.setDisabled(disable); + this.hBoxPassword.setDisabled(disable); + this.hBoxFrom.setDisabled(disable); + this.chkTLS.setDisabled(disable); + this.recipientsFset.getComponent(0).setDisabled(disable); + }, + + onDestroy: function () { + deluge.preferences.un('show', this.updateConfig, this); + + Deluge.ux.preferences.NotificationsPage.superclass.onDestroy.call(this); + }, +}); + +Deluge.plugins.NotificationsPlugin = Ext.extend(Deluge.Plugin, { + name: 'Notifications', + + onDisable: function () { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function () { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.NotificationsPage() + ); + }, +}); + +Deluge.registerPlugin('Notifications', Deluge.plugins.NotificationsPlugin); diff --git a/deluge/plugins/Notifications/deluge_notifications/gtkui.py b/deluge/plugins/Notifications/deluge_notifications/gtkui.py new file mode 100644 index 0000000..4dc5ff8 --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/gtkui.py @@ -0,0 +1,741 @@ +# +# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +from os.path import basename + +from gi import require_version +from gi.repository import Gtk +from twisted.internet import defer + +import deluge.common +import deluge.component as component +import deluge.configmanager +from deluge.plugins.pluginbase import Gtk3PluginBase +from deluge.ui.client import client + +from .common import CustomNotifications, get_resource + +# Relative imports + +log = logging.getLogger(__name__) + +try: + import pygame + + SOUND_AVAILABLE = True +except ImportError: + SOUND_AVAILABLE = False + +try: + require_version('Notify', '0.7') + from gi.repository import GLib, Notify +except (ValueError, ImportError): + POPUP_AVAILABLE = False +else: + POPUP_AVAILABLE = not deluge.common.windows_check() + + +DEFAULT_PREFS = { + # BLINK + 'blink_enabled': False, + # FLASH + 'flash_enabled': False, + # POPUP + 'popup_enabled': False, + # SOUND + 'sound_enabled': False, + 'sound_path': '', + 'custom_sounds': {}, + # Subscriptions + 'subscriptions': {'popup': [], 'blink': [], 'sound': []}, +} + +RECIPIENT_FIELD, RECIPIENT_EDIT = list(range(2)) +( + SUB_EVENT, + SUB_EVENT_DOC, + SUB_NOT_EMAIL, + SUB_NOT_POPUP, + SUB_NOT_BLINK, + SUB_NOT_SOUND, +) = list(range(6)) +SND_EVENT, SND_EVENT_DOC, SND_NAME, SND_PATH = list(range(4)) + + +class GtkUiNotifications(CustomNotifications): + def __init__(self, plugin_name=None): + CustomNotifications.__init__(self, plugin_name) + + def enable(self): + CustomNotifications.enable(self) + self.register_custom_blink_notification( + 'TorrentFinishedEvent', self._on_torrent_finished_event_blink + ) + self.register_custom_sound_notification( + 'TorrentFinishedEvent', self._on_torrent_finished_event_sound + ) + self.register_custom_popup_notification( + 'TorrentFinishedEvent', self._on_torrent_finished_event_popup + ) + + def disable(self): + self.deregister_custom_blink_notification('TorrentFinishedEvent') + self.deregister_custom_sound_notification('TorrentFinishedEvent') + self.deregister_custom_popup_notification('TorrentFinishedEvent') + CustomNotifications.disable(self) + + def register_custom_popup_notification(self, eventtype, handler): + """This is used to register popup notifications for custom event types. + + :param event: the event name + :param type: string + :param handler: function, to be called when `:param:event` is emitted + + Your handler should return a tuple of (popup_title, popup_contents). + """ + self._register_custom_provider('popup', eventtype, handler) + + def deregister_custom_popup_notification(self, eventtype): + self._deregister_custom_provider('popup', eventtype) + + def register_custom_blink_notification(self, eventtype, handler): + """This is used to register blink notifications for custom event types. + + :param event: str, the event name + :param handler: function, to be called when `:param:event` is emitted + + Your handler should return `True` or `False` to blink or not the + trayicon. + """ + self._register_custom_provider('blink', eventtype, handler) + + def deregister_custom_blink_notification(self, eventtype): + self._deregister_custom_provider('blink', eventtype) + + def register_custom_sound_notification(self, eventtype, handler): + """This is used to register sound notifications for custom event types. + + :param event: the event name + :type event: string + :param handler: function to be called when `:param:event` is emitted + + Your handler should return either '' to use the sound defined on the + notification preferences, the path to a sound file, which will then be + played or None, where no sound will be played at all. + """ + self._register_custom_provider('sound', eventtype, handler) + + def deregister_custom_sound_notification(self, eventtype): + self._deregister_custom_provider('sound', eventtype) + + def handle_custom_popup_notification(self, result, eventtype): + title, message = result + return defer.maybeDeferred(self.__popup, title, message) + + def handle_custom_blink_notification(self, result, eventtype): + if result: + return defer.maybeDeferred(self.__blink) + return defer.succeed( + 'Will not blink. The returned value from the custom ' + 'handler was: %s' % result + ) + + def handle_custom_sound_notification(self, result, eventtype): + if isinstance(result, ''.__class__): + if not result and eventtype in self.config['custom_sounds']: + return defer.maybeDeferred( + self.__play_sound, self.config['custom_sounds'][eventtype] + ) + return defer.maybeDeferred(self.__play_sound, result) + return defer.succeed( + 'Will not play sound. The returned value from the ' + 'custom handler was: %s' % result + ) + + def __blink(self): + self.systray.blink(True) + return defer.succeed(_('Notification Blink shown')) + + def __popup(self, title='', message=''): + if not self.config['popup_enabled']: + return defer.succeed(_('Popup notification is not enabled.')) + if not POPUP_AVAILABLE: + err_msg = _('libnotify is not installed') + log.warning(err_msg) + return defer.fail(ImportError(err_msg)) + + if Notify.init('Deluge'): + self.note = Notify.Notification.new(title, message, 'deluge-panel') + self.note.set_hint('desktop-entry', GLib.Variant.new_string('deluge')) + if not self.note.show(): + err_msg = _('Failed to popup notification') + log.warning(err_msg) + return defer.fail(Exception(err_msg)) + return defer.succeed(_('Notification popup shown')) + + def __play_sound(self, sound_path=''): + if not self.config['sound_enabled']: + return defer.succeed(_('Sound notification not enabled')) + if not SOUND_AVAILABLE: + err_msg = _('pygame is not installed') + log.warning(err_msg) + return defer.fail(ImportError(err_msg)) + + pygame.init() + try: + if not sound_path: + sound_path = self.config['sound_path'] + alert_sound = pygame.mixer.music + alert_sound.load(sound_path) + alert_sound.play() + except pygame.error as ex: + err_msg = _('Sound notification failed %s') % ex + log.warning(err_msg) + return defer.fail(ex) + else: + msg = _('Sound notification Success') + log.info(msg) + return defer.succeed(msg) + + def _on_torrent_finished_event_blink(self, torrent_id): + return True # Yes, Blink + + def _on_torrent_finished_event_sound(self, torrent_id): + # Since there's no custom sound hardcoded, just return '' + return '' + + def _on_torrent_finished_event_popup(self, torrent_id): + d = client.core.get_torrent_status(torrent_id, ['name', 'file_progress']) + d.addCallback(self._on_torrent_finished_event_got_torrent_status) + d.addErrback(self._on_torrent_finished_event_torrent_status_failure) + return d + + def _on_torrent_finished_event_torrent_status_failure(self, failure): + log.debug('Failed to get torrent status to be able to show the popup') + + def _on_torrent_finished_event_got_torrent_status(self, torrent_status): + log.debug( + 'Handler for TorrentFinishedEvent GTKUI called. ' 'Got Torrent Status' + ) + title = _('Finished Torrent') + torrent_status['num_files'] = torrent_status['file_progress'].count(1.0) + message = ( + _( + 'The torrent "%(name)s" including %(num_files)i file(s) ' + 'has finished downloading.' + ) + % torrent_status + ) + return title, message + + +class GtkUI(Gtk3PluginBase, GtkUiNotifications): + def __init__(self, plugin_name): + Gtk3PluginBase.__init__(self, plugin_name) + GtkUiNotifications.__init__(self) + + def enable(self): + self.config = deluge.configmanager.ConfigManager( + 'notifications-gtk.conf', DEFAULT_PREFS + ) + self.builder = Gtk.Builder() + self.builder.add_from_file(get_resource('config.ui')) + self.builder.get_object('smtp_port').set_value(25) + self.prefs = self.builder.get_object('prefs_box') + self.prefs.show_all() + + self.build_recipients_model_populate_treeview() + self.build_sounds_model_populate_treeview() + self.build_notifications_model_populate_treeview() + + client.notifications.get_handled_events().addCallback( + self.popuplate_what_needs_handled_events + ) + + self.builder.connect_signals( + { + 'on_add_button_clicked': ( + self.on_add_button_clicked, + self.recipients_treeview, + ), + 'on_delete_button_clicked': ( + self.on_delete_button_clicked, + self.recipients_treeview, + ), + 'on_enabled_toggled': self.on_enabled_toggled, + 'on_sound_enabled_toggled': self.on_sound_enabled_toggled, + 'on_sounds_edit_button_clicked': self.on_sounds_edit_button_clicked, + 'on_sounds_revert_button_clicked': self.on_sounds_revert_button_clicked, + 'on_sound_path_update_preview': self.on_sound_path_update_preview, + } + ) + + component.get('Preferences').add_page(_('Notifications'), self.prefs) + + component.get('PluginManager').register_hook( + 'on_apply_prefs', self.on_apply_prefs + ) + component.get('PluginManager').register_hook( + 'on_show_prefs', self.on_show_prefs + ) + + if not POPUP_AVAILABLE: + self.builder.get_object('popup_enabled').set_property('sensitive', False) + if not SOUND_AVAILABLE: + # for widget_name in ('sound_enabled', 'sound_path', 'sounds_page', 'sounds_page_label'): + # self.builder.get_object(widget_name).set_property('sensitive', False) + self.builder.get_object('sound_enabled').set_property('sensitive', False) + self.builder.get_object('sound_path').set_property('sensitive', False) + self.builder.get_object('sounds_page').set_property('sensitive', False) + self.builder.get_object('sounds_page_label').set_property( + 'sensitive', False + ) + + self.systray = component.get('SystemTray') + if not hasattr(self.systray, 'tray'): + # Tray is not beeing used + self.builder.get_object('blink_enabled').set_property('sensitive', False) + + GtkUiNotifications.enable(self) + + def disable(self): + GtkUiNotifications.disable(self) + component.get('Preferences').remove_page(_('Notifications')) + component.get('PluginManager').deregister_hook( + 'on_apply_prefs', self.on_apply_prefs + ) + component.get('PluginManager').deregister_hook( + 'on_show_prefs', self.on_show_prefs + ) + + def build_recipients_model_populate_treeview(self): + # SMTP Recipients treeview/model + self.recipients_treeview = self.builder.get_object('smtp_recipients') + treeview_selection = self.recipients_treeview.get_selection() + treeview_selection.connect( + 'changed', self.on_recipients_treeview_selection_changed + ) + self.recipients_model = Gtk.ListStore(str, bool) + + renderer = Gtk.CellRendererText() + renderer.connect('edited', self.on_cell_edited, self.recipients_model) + renderer.recipient = RECIPIENT_FIELD + column = Gtk.TreeViewColumn( + 'Recipients', renderer, text=RECIPIENT_FIELD, editable=RECIPIENT_EDIT + ) + column.set_expand(True) + self.recipients_treeview.append_column(column) + self.recipients_treeview.set_model(self.recipients_model) + + def build_sounds_model_populate_treeview(self): + # Sound customisation treeview/model + self.sounds_treeview = self.builder.get_object('sounds_treeview') + sounds_selection = self.sounds_treeview.get_selection() + sounds_selection.connect('changed', self.on_sounds_treeview_selection_changed) + + self.sounds_treeview.set_tooltip_column(SND_EVENT_DOC) + self.sounds_model = Gtk.ListStore(str, str, str, str) + + renderer = Gtk.CellRendererText() + renderer.event = SND_EVENT + column = Gtk.TreeViewColumn('Event', renderer, text=SND_EVENT) + column.set_expand(True) + self.sounds_treeview.append_column(column) + + renderer = Gtk.CellRendererText() + renderer.event_doc = SND_EVENT_DOC + column = Gtk.TreeViewColumn('Doc', renderer, text=SND_EVENT_DOC) + column.set_property('visible', False) + self.sounds_treeview.append_column(column) + + renderer = Gtk.CellRendererText() + renderer.sound_name = SND_NAME + column = Gtk.TreeViewColumn('Name', renderer, text=SND_NAME) + self.sounds_treeview.append_column(column) + + renderer = Gtk.CellRendererText() + renderer.sound_path = SND_PATH + column = Gtk.TreeViewColumn('Path', renderer, text=SND_PATH) + column.set_property('visible', False) + self.sounds_treeview.append_column(column) + + self.sounds_treeview.set_model(self.sounds_model) + + def build_notifications_model_populate_treeview(self): + # Notification Subscriptions treeview/model + self.subscriptions_treeview = self.builder.get_object('subscriptions_treeview') + subscriptions_selection = self.subscriptions_treeview.get_selection() + subscriptions_selection.connect( + 'changed', self.on_subscriptions_treeview_selection_changed + ) + self.subscriptions_treeview.set_tooltip_column(SUB_EVENT_DOC) + self.subscriptions_model = Gtk.ListStore(str, str, bool, bool, bool, bool) + + renderer = Gtk.CellRendererText() + setattr(renderer, 'event', SUB_EVENT) + column = Gtk.TreeViewColumn('Event', renderer, text=SUB_EVENT) + column.set_expand(True) + self.subscriptions_treeview.append_column(column) + + renderer = Gtk.CellRendererText() + setattr(renderer, 'event_doc', SUB_EVENT) + column = Gtk.TreeViewColumn('Doc', renderer, text=SUB_EVENT_DOC) + column.set_property('visible', False) + self.subscriptions_treeview.append_column(column) + + renderer = Gtk.CellRendererToggle() + renderer.set_property('activatable', True) + renderer.connect('toggled', self._on_email_col_toggled) + column = Gtk.TreeViewColumn('Email', renderer, active=SUB_NOT_EMAIL) + column.set_clickable(True) + self.subscriptions_treeview.append_column(column) + + renderer = Gtk.CellRendererToggle() + renderer.set_property('activatable', True) + renderer.connect('toggled', self._on_popup_col_toggled) + column = Gtk.TreeViewColumn('Popup', renderer, active=SUB_NOT_POPUP) + column.set_clickable(True) + self.subscriptions_treeview.append_column(column) + + renderer = Gtk.CellRendererToggle() + renderer.set_property('activatable', True) + renderer.connect('toggled', self._on_blink_col_toggled) + column = Gtk.TreeViewColumn('Blink', renderer, active=SUB_NOT_BLINK) + column.set_clickable(True) + self.subscriptions_treeview.append_column(column) + + renderer = Gtk.CellRendererToggle() + renderer.set_property('activatable', True) + renderer.connect('toggled', self._on_sound_col_toggled) + column = Gtk.TreeViewColumn('Sound', renderer, active=SUB_NOT_SOUND) + column.set_clickable(True) + self.subscriptions_treeview.append_column(column) + self.subscriptions_treeview.set_model(self.subscriptions_model) + + def popuplate_what_needs_handled_events( + self, handled_events, email_subscriptions=None + ): + if email_subscriptions is None: + email_subscriptions = [] + self.populate_subscriptions(handled_events, email_subscriptions) + self.populate_sounds(handled_events) + + def populate_sounds(self, handled_events): + self.sounds_model.clear() + for event_name, event_doc in handled_events: + if event_name in self.config['custom_sounds']: + snd_path = self.config['custom_sounds'][event_name] + else: + snd_path = self.config['sound_path'] + + if snd_path: + self.sounds_model.set( + self.sounds_model.append(), + SND_EVENT, + event_name, + SND_EVENT_DOC, + event_doc, + SND_NAME, + basename(snd_path), + SND_PATH, + snd_path, + ) + + def populate_subscriptions(self, handled_events, email_subscriptions=None): + if email_subscriptions is None: + email_subscriptions = [] + subscriptions_dict = self.config['subscriptions'] + self.subscriptions_model.clear() + # self.handled_events = handled_events + for event_name, event_doc in handled_events: + self.subscriptions_model.set( + self.subscriptions_model.append(), + SUB_EVENT, + event_name, + SUB_EVENT_DOC, + event_doc, + SUB_NOT_EMAIL, + event_name in email_subscriptions, + SUB_NOT_POPUP, + event_name in subscriptions_dict['popup'], + SUB_NOT_BLINK, + event_name in subscriptions_dict['blink'], + SUB_NOT_SOUND, + event_name in subscriptions_dict['sound'], + ) + + def on_apply_prefs(self): + log.debug('applying prefs for Notifications') + + current_popup_subscriptions = [] + current_blink_subscriptions = [] + current_sound_subscriptions = [] + current_email_subscriptions = [] + for event, doc, email, popup, blink, sound in self.subscriptions_model: + if email: + current_email_subscriptions.append(event) + if popup: + current_popup_subscriptions.append(event) + if blink: + current_blink_subscriptions.append(event) + if sound: + current_sound_subscriptions.append(event) + + old_sound_file = self.config['sound_path'] + new_sound_file = self.builder.get_object('sound_path').get_filename() + log.debug( + 'Old Default sound file: %s New one: %s', old_sound_file, new_sound_file + ) + custom_sounds = {} + for event_name, event_doc, filename, filepath in self.sounds_model: + log.debug('Custom sound for event "%s": %s', event_name, filename) + if filepath == old_sound_file: + continue + custom_sounds[event_name] = filepath + + self.config.config.update( + { + 'popup_enabled': self.builder.get_object('popup_enabled').get_active(), + 'blink_enabled': self.builder.get_object('blink_enabled').get_active(), + 'sound_enabled': self.builder.get_object('sound_enabled').get_active(), + 'sound_path': new_sound_file, + 'subscriptions': { + 'popup': current_popup_subscriptions, + 'blink': current_blink_subscriptions, + 'sound': current_sound_subscriptions, + }, + 'custom_sounds': custom_sounds, + } + ) + self.config.save() + + core_config = { + 'smtp_enabled': self.builder.get_object('smtp_enabled').get_active(), + 'smtp_host': self.builder.get_object('smtp_host').get_text(), + 'smtp_port': self.builder.get_object('smtp_port').get_value(), + 'smtp_user': self.builder.get_object('smtp_user').get_text(), + 'smtp_pass': self.builder.get_object('smtp_pass').get_text(), + 'smtp_from': self.builder.get_object('smtp_from').get_text(), + 'smtp_tls': self.builder.get_object('smtp_tls').get_active(), + 'smtp_recipients': [ + dest[0] for dest in self.recipients_model if dest[0] != 'USER@HOST' + ], + 'subscriptions': {'email': current_email_subscriptions}, + } + + client.notifications.set_config(core_config) + client.notifications.get_config().addCallback(self.cb_get_config) + + def on_show_prefs(self): + client.notifications.get_config().addCallback(self.cb_get_config) + + def cb_get_config(self, core_config): + """Callback for on show_prefs.""" + self.builder.get_object('smtp_host').set_text(core_config['smtp_host']) + self.builder.get_object('smtp_port').set_value(core_config['smtp_port']) + self.builder.get_object('smtp_user').set_text(core_config['smtp_user']) + self.builder.get_object('smtp_pass').set_text(core_config['smtp_pass']) + self.builder.get_object('smtp_from').set_text(core_config['smtp_from']) + self.builder.get_object('smtp_tls').set_active(core_config['smtp_tls']) + self.recipients_model.clear() + for recipient in core_config['smtp_recipients']: + self.recipients_model.set( + self.recipients_model.append(), + RECIPIENT_FIELD, + recipient, + RECIPIENT_EDIT, + False, + ) + self.builder.get_object('smtp_enabled').set_active(core_config['smtp_enabled']) + self.builder.get_object('sound_enabled').set_active( + self.config['sound_enabled'] + ) + self.builder.get_object('popup_enabled').set_active( + self.config['popup_enabled'] + ) + self.builder.get_object('blink_enabled').set_active( + self.config['blink_enabled'] + ) + if self.config['sound_path']: + sound_path = self.config['sound_path'] + else: + sound_path = deluge.common.get_default_download_dir() + self.builder.get_object('sound_path').set_filename(sound_path) + # Force toggle + self.on_enabled_toggled(self.builder.get_object('smtp_enabled')) + self.on_sound_enabled_toggled(self.builder.get_object('sound_enabled')) + + client.notifications.get_handled_events().addCallback( + self.popuplate_what_needs_handled_events, + core_config['subscriptions']['email'], + ) + + def on_sound_path_update_preview(self, filechooser): + client.notifications.get_handled_events().addCallback(self.populate_sounds) + + def on_add_button_clicked(self, widget, treeview): + model = treeview.get_model() + model.set(model.append(), RECIPIENT_FIELD, 'USER@HOST', RECIPIENT_EDIT, True) + + def on_delete_button_clicked(self, widget, treeview): + selection = treeview.get_selection() + model, selected_iter = selection.get_selected() + if selected_iter: + model.remove(selected_iter) + + def on_cell_edited(self, cell, path_string, new_text, model): + str_iter = model.get_iter_from_string(path_string) + model.set(str_iter, RECIPIENT_FIELD, new_text) + + def on_recipients_treeview_selection_changed(self, selection): + model, selected_connection_iter = selection.get_selected() + if selected_connection_iter: + self.builder.get_object('delete_button').set_property('sensitive', True) + else: + self.builder.get_object('delete_button').set_property('sensitive', False) + + def on_subscriptions_treeview_selection_changed(self, selection): + model, selected_connection_iter = selection.get_selected() + if selected_connection_iter: + self.builder.get_object('delete_button').set_property('sensitive', True) + else: + self.builder.get_object('delete_button').set_property('sensitive', False) + + def on_sounds_treeview_selection_changed(self, selection): + model, selected_iter = selection.get_selected() + if selected_iter: + self.builder.get_object('sounds_edit_button').set_property( + 'sensitive', True + ) + path = model.get(selected_iter, SND_PATH)[0] + log.debug('Sound selection changed: %s', path) + if path != self.config['sound_path']: + self.builder.get_object('sounds_revert_button').set_property( + 'sensitive', True + ) + else: + self.builder.get_object('sounds_revert_button').set_property( + 'sensitive', False + ) + else: + self.builder.get_object('sounds_edit_button').set_property( + 'sensitive', False + ) + self.builder.get_object('sounds_revert_button').set_property( + 'sensitive', False + ) + + def on_sounds_revert_button_clicked(self, widget): + log.debug('on_sounds_revert_button_clicked') + selection = self.sounds_treeview.get_selection() + model, selected_iter = selection.get_selected() + if selected_iter: + log.debug('on_sounds_revert_button_clicked: got iter') + model.set( + selected_iter, + SND_PATH, + self.config['sound_path'], + SND_NAME, + basename(self.config['sound_path']), + ) + + def on_sounds_edit_button_clicked(self, widget): + log.debug('on_sounds_edit_button_clicked') + selection = self.sounds_treeview.get_selection() + model, selected_iter = selection.get_selected() + if selected_iter: + path = model.get(selected_iter, SND_PATH)[0] + dialog = Gtk.FileChooserDialog( + title=_('Choose Sound File'), + buttons=( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK, + ), + ) + dialog.set_filename(path) + + def update_model(response): + if response == Gtk.ResponseType.OK: + new_filename = dialog.get_filename() + dialog.destroy() + log.debug(new_filename) + model.set( + selected_iter, + SND_PATH, + new_filename, + SND_NAME, + basename(new_filename), + ) + + d = defer.maybeDeferred(dialog.run) + d.addCallback(update_model) + + log.debug('dialog should have been shown') + + def on_enabled_toggled(self, widget): + for widget_name in ( + 'smtp_host', + 'smtp_port', + 'smtp_user', + 'smtp_pass', + 'smtp_pass', + 'smtp_tls', + 'smtp_from', + 'smtp_recipients', + ): + self.builder.get_object(widget_name).set_property( + 'sensitive', widget.get_active() + ) + + def on_sound_enabled_toggled(self, widget): + if widget.get_active(): + self.builder.get_object('sound_path').set_property('sensitive', True) + self.builder.get_object('sounds_page').set_property('sensitive', True) + self.builder.get_object('sounds_page_label').set_property('sensitive', True) + else: + self.builder.get_object('sound_path').set_property('sensitive', False) + self.builder.get_object('sounds_page').set_property('sensitive', False) + self.builder.get_object('sounds_page_label').set_property( + 'sensitive', False + ) + + # for widget_name in ('sounds_path', 'sounds_page', 'sounds_page_label'): + # self.builder.get_object(widget_name).set_property('sensitive', + # widget.get_active()) + + def _on_email_col_toggled(self, cell, path): + self.subscriptions_model[path][SUB_NOT_EMAIL] = not self.subscriptions_model[ + path + ][SUB_NOT_EMAIL] + return + + def _on_popup_col_toggled(self, cell, path): + self.subscriptions_model[path][SUB_NOT_POPUP] = not self.subscriptions_model[ + path + ][SUB_NOT_POPUP] + return + + def _on_blink_col_toggled(self, cell, path): + self.subscriptions_model[path][SUB_NOT_BLINK] = not self.subscriptions_model[ + path + ][SUB_NOT_BLINK] + return + + def _on_sound_col_toggled(self, cell, path): + self.subscriptions_model[path][SUB_NOT_SOUND] = not self.subscriptions_model[ + path + ][SUB_NOT_SOUND] + return diff --git a/deluge/plugins/Notifications/deluge_notifications/test.py b/deluge/plugins/Notifications/deluge_notifications/test.py new file mode 100644 index 0000000..013cdbf --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/test.py @@ -0,0 +1,86 @@ +# vim: sw=4 ts=4 fenc=utf-8 et +# ============================================================================== +# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me> +# +# License: BSD - Please view the LICENSE file for additional information. +# ============================================================================== + +import logging + +from twisted.internet import task + +from deluge import component +from deluge.event import DelugeEvent + +log = logging.getLogger(__name__) + + +class FooEvent(DelugeEvent): + """foo Event""" + + +class CustomEvent(DelugeEvent): + """Just a custom event to test""" + + +class TestEmailNotifications(component.Component): + def __init__(self, imp): + component.Component.__init__(self, self.__class__.__name__, 5) + self.__imp = imp + self.lc = task.LoopingCall(self.update) + self.n = 1 + self.events = [FooEvent(), CustomEvent()] + self.events_classes = [] + + def enable(self): + log.debug('\n\nEnabling %s', self.__class__.__name__) + for event in self.events: + if self.__imp == 'core': + # component.get('CorePlugin.Notifications').register_custom_email_notification( + component.get('Notifications').register_custom_email_notification( + event.__class__.__name__, self.custom_email_message_provider + ) + elif self.__imp == 'gtk': + notifications_component = component.get('Notifications') + notifications_component.register_custom_popup_notification( + event.__class__.__name__, self.custom_popup_message_provider + ) + notifications_component.register_custom_blink_notification( + event.__class__.__name__, self.custom_blink_message_provider + ) + notifications_component.register_custom_sound_notification( + event.__class__.__name__, self.custom_sound_message_provider + ) + + self.lc.start(60, False) + + def disable(self): + log.debug('\n\nDisabling %s', self.__class__.__name__) + self.lc.stop() + + def update(self): + if self.__imp == 'core': + log.debug('\n\nUpdating %s', self.__class__.__name__) + self.events.append(self.events.pop(0)) # Re-Queue + self.n += 1 + component.get('EventManager').emit(self.events[0]) + + def custom_email_message_provider(self, *evt_args, **evt_kwargs): + log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs) + subject = f'{self.events[0].__class__.__name__} Email Subject: {self.n}' + message = f'{self.events[0].__class__.__name__} Email Message: {self.n}' + return subject, message + + def custom_popup_message_provider(self, *evt_args, **evt_kwargs): + log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs) + title = f'{self.events[0].__class__.__name__} Popup Title: {self.n}' + message = f'{self.events[0].__class__.__name__} Popup Message: {self.n}' + return title, message + + def custom_blink_message_provider(self, *evt_args, **evt_kwargs): + log.debug('Running custom blink message provider: %s %s', evt_args, evt_kwargs) + return True + + def custom_sound_message_provider(self, *evt_args, **evt_kwargs): + log.debug('Running custom sound message provider: %s %s', evt_args, evt_kwargs) + return '' diff --git a/deluge/plugins/Notifications/deluge_notifications/webui.py b/deluge/plugins/Notifications/deluge_notifications/webui.py new file mode 100644 index 0000000..bf3e829 --- /dev/null +++ b/deluge/plugins/Notifications/deluge_notifications/webui.py @@ -0,0 +1,31 @@ +# +# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging + +from deluge.plugins.pluginbase import WebPluginBase + +from .common import get_resource + +log = logging.getLogger(__name__) + + +class WebUI(WebPluginBase): + scripts = [get_resource('notifications.js')] + debug_scripts = scripts + + def enable(self): + log.debug('Enabling Web UI notifications') + + def disable(self): + log.debug('Disabling Web UI notifications') |