Skip to content

Commit

Permalink
Windows notification support; refs #14
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Sep 9, 2018
1 parent 804e27c commit 6827045
Show file tree
Hide file tree
Showing 17 changed files with 412 additions and 17 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Expand Up @@ -2,6 +2,7 @@ include LICENSE
include README.md
include README
include requirements.txt
include win-requirements.txt
include dev-requirements.txt
recursive-include test *
global-exclude *.pyc
Expand Down
4 changes: 4 additions & 0 deletions README
Expand Up @@ -119,6 +119,10 @@ The table below identifies the services this tool supports and some example serv
xbmc://user@hostname
xbmc://user:password@hostname:port


* Windows Notifications
windows://

Email Support
-------------
* mailto://
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -42,6 +42,8 @@ The table below identifies the services this tool supports and some example serv
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | tweet:// | (TCP) 443 | tweet://user@CKey/CSecret/AKey/ASecret
| [XBMC](https://github.com/caronc/apprise/wiki/Notify_xbmc) | xbmc:// or xbmcs:// | (TCP) 8080 or 443 | xbmc://hostname<br />xbmc://user@hostname<br />xbmc://user:password@hostname:port
| [Windows Notification](https://github.com/caronc/apprise/wiki/Notify_windows) | windows:// | n/a | windows://


### Email Support
| Service ID | Default Port | Example Syntax |
Expand Down
9 changes: 8 additions & 1 deletion apprise/Apprise.py
Expand Up @@ -204,6 +204,8 @@ def add(self, servers, asset=None):
instance = Apprise.instantiate(_server, asset=asset)
if not instance:
return_status = False
logging.error(
"Failed to load notification url: {}".format(_server))
continue

# Add our initialized plugin to our server listings
Expand Down Expand Up @@ -261,10 +263,15 @@ def notify(self, title, body, notify_type=NotifyType.INFO,
# Toggle our return status flag
status = False

except TypeError:
# These our our internally thrown notifications
# TODO: Change this to a custom one such as AppriseNotifyError
status = False

except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logging.exception("notification exception")
logging.exception("Notification Exception")
status = False

return status
Expand Down
34 changes: 27 additions & 7 deletions apprise/AppriseAsset.py
Expand Up @@ -54,12 +54,16 @@ class AppriseAsset(object):
# The default color to return if a mapping isn't found in our table above
default_html_color = '#888888'

# The default image extension to use
default_extension = '.png'

# The default theme
theme = 'default'

# Image URL Mask
image_url_mask = \
'http://nuxref.com/apprise/themes/{THEME}/apprise-{TYPE}-{XY}.png'
'http://nuxref.com/apprise/themes/{THEME}/' \
'apprise-{TYPE}-{XY}{EXTENSION}'

# Application Logo
image_url_logo = \
Expand All @@ -71,11 +75,11 @@ class AppriseAsset(object):
'assets',
'themes',
'{THEME}',
'apprise-{TYPE}-{XY}.png',
'apprise-{TYPE}-{XY}{EXTENSION}',
))

def __init__(self, theme='default', image_path_mask=None,
image_url_mask=None):
image_url_mask=None, default_extension=None):
"""
Asset Initialization
Expand All @@ -89,6 +93,9 @@ def __init__(self, theme='default', image_path_mask=None,
if image_url_mask is not None:
self.image_url_mask = image_url_mask

if default_extension is not None:
self.default_extension = default_extension

def color(self, notify_type, color_type=None):
"""
Returns an HTML mapped color based on passed in notify type
Expand Down Expand Up @@ -121,7 +128,7 @@ def color(self, notify_type, color_type=None):
raise ValueError(
'AppriseAsset html_color(): An invalid color_type was specified.')

def image_url(self, notify_type, image_size, logo=False):
def image_url(self, notify_type, image_size, logo=False, extension=None):
"""
Apply our mask to our image URL
Expand All @@ -134,10 +141,14 @@ def image_url(self, notify_type, image_size, logo=False):
# No image to return
return None

if extension is None:
extension = self.default_extension

re_map = {
'{THEME}': self.theme if self.theme else '',
'{TYPE}': notify_type,
'{XY}': image_size,
'{EXTENSION}': extension,
}

# Iterate over above list and store content accordingly
Expand All @@ -148,7 +159,8 @@ def image_url(self, notify_type, image_size, logo=False):

return re_table.sub(lambda x: re_map[x.group()], url_mask)

def image_path(self, notify_type, image_size, must_exist=True):
def image_path(self, notify_type, image_size, must_exist=True,
extension=None):
"""
Apply our mask to our image file path
Expand All @@ -158,10 +170,14 @@ def image_path(self, notify_type, image_size, must_exist=True):
# No image to return
return None

if extension is None:
extension = self.default_extension

re_map = {
'{THEME}': self.theme if self.theme else '',
'{TYPE}': notify_type,
'{XY}': image_size,
'{EXTENSION}': extension,
}

# Iterate over above list and store content accordingly
Expand All @@ -178,13 +194,17 @@ def image_path(self, notify_type, image_size, must_exist=True):
# Return what we parsed
return path

def image_raw(self, notify_type, image_size):
def image_raw(self, notify_type, image_size, extension=None):
"""
Returns the raw image if it can (otherwise the function returns None)
"""

path = self.image_path(notify_type=notify_type, image_size=image_size)
path = self.image_path(
notify_type=notify_type,
image_size=image_size,
extension=extension,
)
if path:
try:
with open(path, 'rb') as fd:
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 6 additions & 3 deletions apprise/plugins/NotifyBase.py
Expand Up @@ -171,7 +171,7 @@ def throttle(self, throttle_time=None):

return

def image_url(self, notify_type, logo=False):
def image_url(self, notify_type, logo=False, extension=None):
"""
Returns Image URL if possible
"""
Expand All @@ -186,9 +186,10 @@ def image_url(self, notify_type, logo=False):
notify_type=notify_type,
image_size=self.image_size,
logo=logo,
extension=extension,
)

def image_path(self, notify_type):
def image_path(self, notify_type, extension=None):
"""
Returns the path of the image if it can
"""
Expand All @@ -201,9 +202,10 @@ def image_path(self, notify_type):
return self.asset.image_path(
notify_type=notify_type,
image_size=self.image_size,
extension=extension,
)

def image_raw(self, notify_type):
def image_raw(self, notify_type, extension=None):
"""
Returns the raw image if it can
"""
Expand All @@ -216,6 +218,7 @@ def image_raw(self, notify_type):
return self.asset.image_raw(
notify_type=notify_type,
image_size=self.image_size,
extension=extension,
)

def color(self, notify_type, color_type=None):
Expand Down
185 changes: 185 additions & 0 deletions apprise/plugins/NotifyWindows.py
@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
#
# Windows Notify Wrapper
#
# Copyright (C) 2017-2018 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import re
from time import sleep

from .NotifyBase import NotifyBase
from ..common import NotifyImageSize

# Default our global support flag
NOTIFY_WINDOWS_SUPPORT_ENABLED = False

try:
# 3rd party modules (Windows Only)
import win32api
import win32con
import win32gui

# We're good to go!
NOTIFY_WINDOWS_SUPPORT_ENABLED = True

except ImportError:
# No problem; we just simply can't support this plugin because we're
# either using Linux, or simply do not have pypiwin32 installed.
pass


class NotifyWindows(NotifyBase):
"""
A wrapper for local Windows Notifications
"""

# The default protocol
protocol = 'windows'

# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128

# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the
# packages actually are present but we need to test that they aren't.
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_windows_plugin.py, please
# let me know! :)
_enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED

def __init__(self, **kwargs):
"""
Initialize Windows Object
"""

# Number of seconds to display notification for
self.duration = 12

# Define our handler
self.hwnd = None

super(NotifyWindows, self).__init__(**kwargs)

def _on_destroy(self, hwnd, msg, wparam, lparam):
"""
Destroy callback function
"""

nid = (self.hwnd, 0)
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid)
win32api.PostQuitMessage(0)

return None

def notify(self, title, body, notify_type, **kwargs):
"""
Perform Windows Notification
"""

if not self._enabled:
self.logger.warning(
"Windows Notifications are not supported by this system.")
return False

# Limit results to just the first 2 line otherwise
# there is just to much content to display
body = re.split('[\r\n]+', body)
body[0] = body[0].strip('#').strip()
body = '\r\n'.join(body[0:2])

try:
# Register destruction callback
message_map = {win32con.WM_DESTROY: self._on_destroy, }

# Register the window class.
self.wc = win32gui.WNDCLASS()
self.hinst = self.wc.hInstance = win32api.GetModuleHandle(None)
self.wc.lpszClassName = str("PythonTaskbar")
self.wc.lpfnWndProc = message_map
self.classAtom = win32gui.RegisterClass(self.wc)

# Styling and window type
style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
self.hwnd = win32gui.CreateWindow(
self.classAtom, "Taskbar", style, 0, 0,
win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, 0, 0,
self.hinst, None)
win32gui.UpdateWindow(self.hwnd)

# image path
icon_path = self.image_path(notify_type, extension='.ico')
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE

try:
hicon = win32gui.LoadImage(
self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0,
icon_flags)

except Exception as e:
self.logger.warning(
"Could not load windows notification icon ({}): {}"
.format(icon_path, e))

# disable icon
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)

# Taskbar icon
flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP
nid = (self.hwnd, 0, flags, win32con.WM_USER + 20, hicon,
"Tooltip")
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid)
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, (
self.hwnd, 0, win32gui.NIF_INFO, win32con.WM_USER + 20, hicon,
"Balloon Tooltip", body, 200, title))

# take a rest then destroy
sleep(self.duration)
win32gui.DestroyWindow(self.hwnd)
win32gui.UnregisterClass(self.wc.lpszClassName, None)

self.logger.info('Sent Windows notification.')

except Exception as e:
self.logger.warning('Failed to send Windows notification.')
self.logger.exception('Windows Exception')
return False

return True

@staticmethod
def parse_url(url):
"""
There are no parameters nessisary for this protocol; simply having
windows:// is all you need. This function just makes sure that
is in place.
"""

# return a very basic set of requirements
return {
'schema': NotifyWindows.protocol,
'user': None,
'password': None,
'port': None,
'host': 'localhost',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}

0 comments on commit 6827045

Please sign in to comment.