mirror of https://github.com/caronc/apprise
Chris Caron
6 years ago
17 changed files with 412 additions and 17 deletions
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 88 KiB |
@ -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,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*- |
||||
# |
||||
# NotifyWindows - Unit Tests |
||||
# |
||||
# Copyright (C) 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. |
||||
|
||||
import mock |
||||
import sys |
||||
import types |
||||
|
||||
# Rebuild our Apprise environment |
||||
import apprise |
||||
|
||||
try: |
||||
# Python v3.4+ |
||||
from importlib import reload |
||||
except ImportError: |
||||
try: |
||||
# Python v3.0-v3.3 |
||||
from imp import reload |
||||
except ImportError: |
||||
# Python v2.7 |
||||
pass |
||||
|
||||
|
||||
def test_windows_plugin(): |
||||
""" |
||||
API: NotifyWindows Plugin() |
||||
|
||||
""" |
||||
|
||||
# We need to fake our windows environment for testing purposes |
||||
win32api_name = 'win32api' |
||||
win32api = types.ModuleType(win32api_name) |
||||
sys.modules[win32api_name] = win32api |
||||
win32api.GetModuleHandle = mock.Mock( |
||||
name=win32api_name + '.GetModuleHandle') |
||||
win32api.PostQuitMessage = mock.Mock( |
||||
name=win32api_name + '.PostQuitMessage') |
||||
|
||||
win32con_name = 'win32con' |
||||
win32con = types.ModuleType(win32con_name) |
||||
sys.modules[win32con_name] = win32con |
||||
win32con.CW_USEDEFAULT = mock.Mock(name=win32con_name + '.CW_USEDEFAULT') |
||||
win32con.IDI_APPLICATION = mock.Mock( |
||||
name=win32con_name + '.IDI_APPLICATION') |
||||
win32con.IMAGE_ICON = mock.Mock(name=win32con_name + '.IMAGE_ICON') |
||||
win32con.LR_DEFAULTSIZE = 1 |
||||
win32con.LR_LOADFROMFILE = 2 |
||||
win32con.WM_DESTROY = mock.Mock(name=win32con_name + '.WM_DESTROY') |
||||
win32con.WM_USER = 0 |
||||
win32con.WS_OVERLAPPED = 1 |
||||
win32con.WS_SYSMENU = 2 |
||||
|
||||
win32gui_name = 'win32gui' |
||||
win32gui = types.ModuleType(win32gui_name) |
||||
sys.modules[win32gui_name] = win32gui |
||||
win32gui.CreateWindow = mock.Mock(name=win32gui_name + '.CreateWindow') |
||||
win32gui.DestroyWindow = mock.Mock(name=win32gui_name + '.DestroyWindow') |
||||
win32gui.LoadIcon = mock.Mock(name=win32gui_name + '.LoadIcon') |
||||
win32gui.LoadImage = mock.Mock(name=win32gui_name + '.LoadImage') |
||||
win32gui.NIF_ICON = 1 |
||||
win32gui.NIF_INFO = mock.Mock(name=win32gui_name + '.NIF_INFO') |
||||
win32gui.NIF_MESSAGE = 2 |
||||
win32gui.NIF_TIP = 4 |
||||
win32gui.NIM_ADD = mock.Mock(name=win32gui_name + '.NIM_ADD') |
||||
win32gui.NIM_DELETE = mock.Mock(name=win32gui_name + '.NIM_DELETE') |
||||
win32gui.NIM_MODIFY = mock.Mock(name=win32gui_name + '.NIM_MODIFY') |
||||
win32gui.RegisterClass = mock.Mock(name=win32gui_name + '.RegisterClass') |
||||
win32gui.UnregisterClass = mock.Mock( |
||||
name=win32gui_name + '.UnregisterClass') |
||||
win32gui.Shell_NotifyIcon = mock.Mock( |
||||
name=win32gui_name + '.Shell_NotifyIcon') |
||||
win32gui.UpdateWindow = mock.Mock(name=win32gui_name + '.UpdateWindow') |
||||
win32gui.WNDCLASS = mock.Mock(name=win32gui_name + '.WNDCLASS') |
||||
|
||||
# The following allows our mocked content to kick in. In python 3.x keys() |
||||
# returns an iterator, therefore we need to convert the keys() back into |
||||
# a list object to prevent from getting the error: |
||||
# "RuntimeError: dictionary changed size during iteration" |
||||
# |
||||
for mod in list(sys.modules.keys()): |
||||
if mod.startswith('apprise.'): |
||||
del(sys.modules[mod]) |
||||
reload(apprise) |
||||
|
||||
# Create our instance |
||||
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False) |
||||
obj.duration = 0 |
||||
|
||||
# Check that it found our mocked environments |
||||
assert(obj._enabled is True) |
||||
|
||||
# _on_destroy check |
||||
obj._on_destroy(0, '', 0, 0) |
||||
|
||||
# test notifications |
||||
assert(obj.notify(title='title', body='body', |
||||
notify_type=apprise.NotifyType.INFO) is True) |
||||
|
||||
# Test our loading of our icon exception; it will still allow the |
||||
# notification to be sent |
||||
win32gui.LoadImage.side_effect = AttributeError |
||||
assert(obj.notify(title='title', body='body', |
||||
notify_type=apprise.NotifyType.INFO) is True) |
||||
# Undo our change |
||||
win32gui.LoadImage.side_effect = None |
||||
|
||||
# Test our global exception handling |
||||
win32gui.UpdateWindow.side_effect = AttributeError |
||||
assert(obj.notify(title='title', body='body', |
||||
notify_type=apprise.NotifyType.INFO) is False) |
||||
# Undo our change |
||||
win32gui.UpdateWindow.side_effect = None |
||||
|
||||
# Toggle our testing for when we can't send notifications because the |
||||
# package has been made unavailable to us |
||||
obj._enabled = False |
||||
assert(obj.notify(title='title', body='body', |
||||
notify_type=apprise.NotifyType.INFO) is False) |
Loading…
Reference in new issue