diff --git a/CMakeLists.txt b/CMakeLists.txt index bf71861..498226a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,35 +1,37 @@ cmake_minimum_required(VERSION 3.4) -project( snoretoast VERSION 0.5.99) +project(snoretoast VERSION 0.6.0) +# Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID +set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/) option(BUILD_EXAMPLES "Whether to build the examples" OFF) option(BUILD_STATIC_RUNTIME "Whether link statically to the msvc runtime" ON) include(GenerateExportHeader) include(SnoreMacros) include(cmakerc/CMakeRC) set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) if (BUILD_STATIC_RUNTIME) #link runtime static if(MSVC) foreach(_bt DEBUG RELEASE RELWITHDEBINFO) string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_${_bt} ${CMAKE_CXX_FLAGS_${_bt}}) endforeach(_bt DEBUG RELEASE RELWITHDEBINFO) endif(MSVC) endif() add_subdirectory(data) add_subdirectory(src) if (BUILD_EXAMPLES) add_subdirectory(examples) endif() diff --git a/README.md b/README.md index e0c3853..f714704 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,89 @@ ![binary-factory-status](https://binary-factory.kde.org/job/SnoreToast_Nightly_win64/badge/icon) Snoretoast![Logo](data/96-96-snoretoast.png) ========== A command line application capable of creating Windows Toast notifications on Windows 8 or later. If SnoreToast is used without the parameter --appID an default appID is used and a shortcut to SnoreToast.exe is created in the startmenu, notifications created that way will be asigned to SnoreToast. If you want to brand your notifications you need to create the application startmenu entry with `snoretoast.exe --install "MyApp\MyApp.lnk" "C:\myApp.exe" "My.APP_ID"`. This appID then needs to be passed to snoretoast.exe with the `--appID`` parameter. # Releases and Binaries Releases and binaries can be found [here](https://download.kde.org/stable/snoretoast/). # Contact us - [Repot Bugs](https://bugs.kde.org/enter_bug.cgi?product=Snoretoast) - [Find us on Irc](http://webchat.freenode.net/?channels=%23kde-windows) - [Send us a mail](mailto:kde-windows@kde.org) ---------------------------------------------------------- ``` -Welcome to SnoreToast 0.5.99. +Welcome to SnoreToast 0.6.0. A command line application capable of creating Windows Toast notifications. ---- Usage ---- SnoreToast [Options] ---- Options ---- [-t] | Displayed on the first line of the toast. [-m] <message string> | Displayed on the remaining lines, wrapped. [-b] <button1;button2 string>| Displayed on the bottom line, can list multiple buttons separated by ; [-tb] | Displayed a textbox on the bottom line, only if buttons are not presented. [-p] <image URI> | Display toast with an image, local files only. [-id] <id> | sets the id for a notification to be able to close it later. [-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx. [-silent] | Don't play a sound file when showing the notifications. [-appID] <App.ID> | Don't create a shortcut but use the provided app id. [-pipeName] <\.\pipe\pipeName\> | Provide a name pipe which is used for callbacks. [-application] <C:\foo.exe> | Provide a application that might be started if the pipe does not exist. -close <id> | Closes a currently displayed notification. -install <name> <application> <appID>| Creates a shortcut <name> in the start menu which point to the executable <application>, appID used for the notifications. -v | Print the version and copying information. -h | Print these instructions. Same as no args. Exit Status : Exit Code Failed : -1 Success : 0 Hidden : 1 Dismissed : 2 TimedOut : 3 ButtonPressed : 4 TextEntered : 5 ---- Image Notes ---- Images must be .png with: maximum dimensions of 1024x1024 size <= 200kb These limitations are due to the Toast notification system. ``` ---------------------------------------------------------- # Shortcut creation with Nsis ``` !include LogicLib.nsh !include WordFunc.nsh Function SnoreWinVer ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" CurrentVersion ${VersionCompare} "6.2" $R0 $R0 ${If} $R0 == 1 Push "NotWin8" ${Else} Push "AtLeastWin8" ${EndIf} FunctionEnd !macro SnoreShortcut path exe appID Call SnoreWinVer Pop $0 ${If} $0 == "AtLeastWin8" nsExec::ExecToLog '"${SnoreToastExe}" -install "${path}" "${exe}" "${appID}"' ${Else} CreateShortCut "${path}" "${exe}" ${EndIf} !macroend ``` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index efec8b6..eef57f7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,37 +1,26 @@ add_library(SnoreToastActions INTERFACE) target_include_directories(SnoreToastActions INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include/snoretoast> ) add_library(SnoreToast::SnoreToastActions ALIAS SnoreToastActions) - +configure_file(config.h.in config.h @ONLY) add_library(libsnoretoast STATIC snoretoasts.cpp toasteventhandler.cpp linkhelper.cpp utils.cpp) target_link_libraries(libsnoretoast PUBLIC runtimeobject shlwapi SnoreToast::SnoreToastActions) target_compile_definitions(libsnoretoast PRIVATE UNICODE _UNICODE __WRL_CLASSIC_COM_STRICT__ WIN32_LEAN_AND_MEAN NOMINMAX) -target_compile_definitions(libsnoretoast PRIVATE - SNORETOAST_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} - SNORETOAST_VERSION_MINOR=${PROJECT_VERSION_MINOR} - SNORETOAST_VERSION_PATCH=${PROJECT_VERSION_PATCH} -) target_compile_definitions(libsnoretoast PUBLIC __WRL_CLASSIC_COM_STRICT__) target_include_directories(libsnoretoast PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>) set_target_properties(libsnoretoast PROPERTIES EXPORT_NAME LibSnoreToast) add_library(SnoreToast::LibSnoreToast ALIAS libsnoretoast) - - generate_export_header(libsnoretoast) create_icon_rc(${PROJECT_SOURCE_DIR}/data/zzz.ico TOAST_ICON) add_executable(snoretoast WIN32 main.cpp ${TOAST_ICON}) target_link_libraries(snoretoast PRIVATE SnoreToast::LibSnoreToast snoreretoastsources) target_compile_definitions(snoretoast PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) -# if there are changes to the callback mechanism we need to change the uuid for the activator SNORETOAST_CALLBACK_UUID -target_compile_definitions(snoretoast PRIVATE - SNORETOAST_CALLBACK_UUID="{383803B6-AFDA-4220-BFC3-0DBF810106BF}" -) add_executable(SnoreToast::SnoreToast ALIAS snoretoast) install(TARGETS snoretoast SnoreToastActions EXPORT LibSnoreToastConfig RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) -install(FILES snoretoastactions.h DESTINATION include/snoretoast) +install(FILES snoretoastactions.h ${CMAKE_CURRENT_BINARY_DIR}/config.h DESTINATION include/snoretoast) install(EXPORT LibSnoreToastConfig DESTINATION lib/cmake/libsnoretoast NAMESPACE SnoreToast::) diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..82e0421 --- /dev/null +++ b/src/config.h.in @@ -0,0 +1,27 @@ +/* + SnoreToast is capable to invoke Windows 8 toast notifications. + Copyright (C) 2019 Hannah von Reth <vonreth@kde.org> + + SnoreToast 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. + + SnoreToast 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. + + You should have received a copy of the GNU Lesser General Public License + along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + + +#define SNORETOAST_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ +#define SNORETOAST_VERSION_MINOR @PROJECT_VERSION_MINOR@ +#define SNORETOAST_VERSION_PATCH @PROJECT_VERSION_PATCH@ + + +#define SNORETOAST_CALLBACK_GUID "@SNORETOAST_CALLBACK_GUID@" diff --git a/src/main.cpp b/src/main.cpp index 1649491..2952617 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,333 +1,335 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast 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. SnoreToast 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. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #include "snoretoasts.h" +#include "config.h" + #include "toasteventhandler.h" #include "snoretoastactioncenterintegration.h" #include "linkhelper.h" #include <cmrc/cmrc.hpp> #include <shellapi.h> #include <roapi.h> #include <algorithm> #include <functional> #include <fstream> #include <iostream> #include <sstream> #include <string> #include <vector> CMRC_DECLARE(SnoreToastResource); void help(const std::wstring &error) { if (!error.empty()) { std::wcerr << error << std::endl; } else { std::wcerr << L"Welcome to SnoreToast " << SnoreToasts::version() << "." << std::endl << L"A command line application capable of creating Windows Toast notifications." << std::endl; } std::wcerr << std::endl << L"---- Usage ----" << std::endl << L"SnoreToast [Options]" << std::endl << std::endl << L"---- Options ----" << std::endl << L"[-t] <title string>\t| Displayed on the first line of the toast." << std::endl << L"[-m] <message string>\t| Displayed on the remaining lines, wrapped." << std::endl << L"[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple " L"buttons separated by ;" << std::endl << L"[-tb]\t\t\t| Displayed a textbox on the bottom line, only if buttons are not " L"presented." << std::endl << L"[-p] <image URI>\t| Display toast with an image, local files only." << std::endl << L"[-id] <id>\t\t| sets the id for a notification to be able to close it later." << std::endl << L"[-s] <sound URI> \t| Sets the sound of the notifications, for possible values see " L"http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx." << std::endl << L"[-silent] \t\t| Don't play a sound file when showing the notifications." << std::endl << L"[-appID] <App.ID>\t| Don't create a shortcut but use the provided app id." << std::endl << L"[-pipeName] <\\.\\pipe\\pipeName\\>\t| Provide a name pipe which is used for " L"callbacks." << std::endl << L"[-application] <C:\\foo.exe>\t| Provide a application that might be started if " L"the pipe does not exist." << std::endl << L"-close <id>\t\t| Closes a currently displayed notification." << std::endl << std::endl << L"-install <name> <application> <appID>| Creates a shortcut <name> in the start " L"menu which point to the executable <application>, appID used for the " L"notifications." << std::endl << std::endl << L"-v \t\t\t| Print the version and copying information." << std::endl << L"-h\t\t\t| Print these instructions. Same as no args." << std::endl << L"Exit Status\t: Exit Code" << std::endl << L"Failed\t\t: " << static_cast<int>(SnoreToastActions::Actions::Error) << std::endl << std::endl << "Success\t\t: " << static_cast<int>(SnoreToastActions::Actions::Clicked) << std::endl << "Hidden\t\t: " << static_cast<int>(SnoreToastActions::Actions::Hidden) << std::endl << "Dismissed\t: " << static_cast<int>(SnoreToastActions::Actions::Dismissed) << std::endl << "TimedOut\t: " << static_cast<int>(SnoreToastActions::Actions::Timedout) << std::endl << "ButtonPressed\t: " << static_cast<int>(SnoreToastActions::Actions::ButtonClicked) << std::endl << "TextEntered\t: " << static_cast<int>(SnoreToastActions::Actions::TextEntered) << std::endl << std::endl << L"---- Image Notes ----" << std::endl << L"Images must be .png with:" << std::endl << L"\tmaximum dimensions of 1024x1024" << std::endl << L"\tsize <= 200kb" << std::endl << L"These limitations are due to the Toast notification system." << std::endl; } void version() { std::wcerr << L"SnoreToast version " << SnoreToasts::version() << std::endl << L"Copyright (C) 2019 Hannah von Reth <vonreth@kde.org>" << std::endl << L"SnoreToast is free software: you can redistribute it and/or modify" << std::endl << L"it under the terms of the GNU Lesser General Public License as published by" << std::endl << L"the Free Software Foundation, either version 3 of the License, or" << std::endl << L"(at your option) any later version." << std::endl; } std::filesystem::path getIcon() { auto image = std::filesystem::temp_directory_path() / "snoretoast" / SnoreToasts::version() / "logo.png"; if (!std::filesystem::exists(image)) { std::filesystem::create_directories(image.parent_path()); const auto filesystem = cmrc::SnoreToastResource::get_filesystem(); const auto img = filesystem.open("256-256-snoretoast.png"); std::ofstream out(image, std::ios::binary); out.write(const_cast<char *>(img.begin()), img.size()); out.close(); } return image; } SnoreToastActions::Actions parse(std::vector<wchar_t *> args) { HRESULT hr = S_OK; std::wstring appID; std::filesystem::path pipe; std::filesystem::path application; std::wstring title; std::wstring body; std::filesystem::path image; std::wstring id; std::wstring sound(L"Notification.Default"); std::wstring buttons; bool silent = false; bool closeNotify = false; bool isTextBoxEnabled = false; auto nextArg = [&](std::vector<wchar_t *>::const_iterator &it, const std::wstring &helpText) -> std::wstring { if (it != args.cend()) { return *it++; } else { help(helpText); exit(static_cast<int>(SnoreToastActions::Actions::Error)); } }; auto it = args.begin() + 1; while (it != args.end()) { std::wstring arg(nextArg(it, L"")); std::transform(arg.begin(), arg.end(), arg.begin(), [](int i) -> int { return ::tolower(i); }); if (arg == L"-m") { body = nextArg(it, L"Missing argument to -m.\n" L"Supply argument as -m \"message string\""); } else if (arg == L"-t") { title = nextArg(it, L"Missing argument to -t.\n" L"Supply argument as -t \"bold title string\""); } else if (arg == L"-p") { image = nextArg(it, L"Missing argument to -p. Supply argument as -p \"image path\""); } else if (arg == L"-s") { sound = nextArg(it, L"Missing argument to -s.\n" L"Supply argument as -s \"sound name\""); } else if (arg == L"-id") { id = nextArg(it, L"Missing argument to -id.\n" L"Supply argument as -id \"id\""); } else if (arg == L"-silent") { silent = true; } else if (arg == L"-appid") { appID = nextArg(it, L"Missing argument to -appID.\n" L"Supply argument as -appID \"Your.APP.ID\""); } else if (arg == L"-pipename") { pipe = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -pipeName \"\\.\\pipe\\foo\\\""); } else if (arg == L"-application") { application = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -applicatzion \"C:\\foo.exe\""); } else if (arg == L"-b") { buttons = nextArg(it, L"Missing argument to -b.\n" L"Supply argument for buttons as -b \"button1;button2\""); } else if (arg == L"-tb") { isTextBoxEnabled = true; } else if (arg == L"-install") { const std::wstring shortcut( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); const std::wstring exe( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); appID = nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\""); return SUCCEEDED(LinkHelper::tryCreateShortcut( shortcut, exe, appID, SnoreToastActionCenterIntegration::uuid())) ? SnoreToastActions::Actions::Clicked : SnoreToastActions::Actions::Error; } else if (arg == L"-close") { id = nextArg(it, L"Missing agument to -close" L"Supply argument as -close \"id\""); closeNotify = true; } else if (arg == L"-v") { version(); return SnoreToastActions::Actions::Clicked; } else if (arg == L"-h") { help(L""); return SnoreToastActions::Actions::Clicked; } else { std::wstringstream ws; ws << L"Unknown argument: " << arg << std::endl; help(ws.str()); return SnoreToastActions::Actions::Error; } } if (appID.empty()) { std::wstringstream _appID; _appID << L"Snore.DesktopToasts." << SnoreToasts::version(); appID = _appID.str(); hr = LinkHelper::tryCreateShortcut(std::filesystem::path(L"SnoreToast") / SnoreToasts::version() / L"SnoreToast", appID, SnoreToastActionCenterIntegration::uuid()); if (!SUCCEEDED(hr)) { return SnoreToastActions::Actions::Error; } } if (closeNotify) { if (!id.empty()) { SnoreToasts app(appID); app.setId(id); if (app.closeNotification()) { return SnoreToastActions::Actions::Clicked; } } else { help(L"Close only works if an -id id was provided."); } } else { hr = (title.length() > 0 && body.length() > 0) ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { if (isTextBoxEnabled) { if (pipe.empty()) { std::wcerr << L"TextBox notifications only work if a pipe for the result " L"was provided" << std::endl; return SnoreToastActions::Actions::Error; } } if (image.empty()) { image = getIcon(); } SnoreToasts app(appID); app.setPipeName(pipe); app.setApplication(application); app.setSilent(silent); app.setSound(sound); app.setId(id); app.setButtons(buttons); app.setTextBoxEnabled(isTextBoxEnabled); app.displayToast(title, body, image); return app.userAction(); } else { help(L""); return SnoreToastActions::Actions::Clicked; } } return SnoreToastActions::Actions::Error; } SnoreToastActions::Actions handleEmbedded() { SnoreToasts::waitForCallbackActivation(); return SnoreToastActions::Actions::Clicked; } int WINAPI wWinMain(HINSTANCE, HINSTANCE, wchar_t *, int) { if (AttachConsole(ATTACH_PARENT_PROCESS)) { FILE *dummy; _wfreopen_s(&dummy, L"CONOUT$", L"w", stdout); setvbuf(stdout, nullptr, _IONBF, 0); _wfreopen_s(&dummy, L"CONOUT$", L"w", stderr); setvbuf(stderr, nullptr, _IONBF, 0); std::ios::sync_with_stdio(); } const auto commandLine = GetCommandLineW(); int argc; wchar_t **argv = CommandLineToArgvW(commandLine, &argc); SnoreToastActions::Actions action = SnoreToastActions::Actions::Clicked; HRESULT hr = Windows::Foundation::Initialize(RO_INIT_MULTITHREADED); if (SUCCEEDED(hr)) { if (std::wstring(commandLine).find(L"-Embedding") != std::wstring::npos) { action = handleEmbedded(); } else { action = parse(std::vector<wchar_t *>(argv, argv + argc)); } Windows::Foundation::Uninitialize(); } return static_cast<int>(action); } diff --git a/src/snoretoastactioncenterintegration.h b/src/snoretoastactioncenterintegration.h index 43edc3f..f11ceeb 100644 --- a/src/snoretoastactioncenterintegration.h +++ b/src/snoretoastactioncenterintegration.h @@ -1,79 +1,79 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2019 Hannah von Reth <vonreth@kde.org> SnoreToast 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. SnoreToast 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. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #pragma once #include <algorithm> #include <ntverp.h> #include <sstream> #include <wrl.h> typedef struct NOTIFICATION_USER_INPUT_DATA { LPCWSTR Key; LPCWSTR Value; } NOTIFICATION_USER_INPUT_DATA; MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") INotificationActivationCallback : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE Activate( __RPC__in_string LPCWSTR appUserModelId, __RPC__in_opt_string LPCWSTR invokedArgs, __RPC__in_ecount_full_opt(count) const NOTIFICATION_USER_INPUT_DATA *data, ULONG count) = 0; }; // The COM server which implements the callback notifcation from Action Center -class DECLSPEC_UUID(SNORETOAST_CALLBACK_UUID) SnoreToastActionCenterIntegration +class DECLSPEC_UUID(SNORETOAST_CALLBACK_GUID) SnoreToastActionCenterIntegration : public Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, INotificationActivationCallback> { public: static std::wstring uuid() { static std::wstring _uuid = [] { std::wstringstream out; - out << SNORETOAST_CALLBACK_UUID; + out << SNORETOAST_CALLBACK_GUID; return out.str(); }(); return _uuid; } SnoreToastActionCenterIntegration() {} virtual HRESULT STDMETHODCALLTYPE Activate(__RPC__in_string LPCWSTR appUserModelId, __RPC__in_opt_string LPCWSTR invokedArgs, __RPC__in_ecount_full_opt(count) const NOTIFICATION_USER_INPUT_DATA *data, ULONG count) override { if (invokedArgs == nullptr) { return S_OK; } std::wstringstream msg; for (ULONG i = 0; i < count; ++i) { std::wstring tmp = data[i].Value; // printing \r to stdcout is kind of problematic :D std::replace(tmp.begin(), tmp.end(), L'\r', L'\n'); msg << tmp; } return SnoreToasts::backgroundCallback(appUserModelId, invokedArgs, msg.str()); } }; CoCreatableClass(SnoreToastActionCenterIntegration); diff --git a/src/snoretoasts.cpp b/src/snoretoasts.cpp index 88035ed..0d4e009 100644 --- a/src/snoretoasts.cpp +++ b/src/snoretoasts.cpp @@ -1,609 +1,610 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast 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. SnoreToast 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. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. - */ +*/ #include "snoretoasts.h" #include "toasteventhandler.h" #include "linkhelper.h" #include "utils.h" +#include "config.h" #include <wrl\wrappers\corewrappers.h> #include <sstream> #include <iostream> using namespace Microsoft::WRL; using namespace ABI::Windows::UI; using namespace ABI::Windows::UI::Notifications; using namespace ABI::Windows::Data::Xml::Dom; using namespace Windows::Foundation; using namespace Wrappers; namespace { constexpr DWORD EVENT_TIMEOUT = 60 * 1000; // one minute should be more than enough } class SnoreToastsPrivate { public: SnoreToastsPrivate(SnoreToasts *parent, const std::wstring &appID) : m_parent(parent), m_appID(appID), m_id(std::to_wstring(GetCurrentProcessId())) { HRESULT hr = GetActivationFactory( HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) .Get(), &m_toastManager); if (!SUCCEEDED(hr)) { std::wcerr << L"SnoreToasts: Failed to register com Factory, please make sure you " L"correctly initialised with RO_INIT_MULTITHREADED" << std::endl; m_action = SnoreToastActions::Actions::Error; } } SnoreToasts *m_parent; std::wstring m_appID; std::filesystem::path m_pipeName; std::filesystem::path m_application; std::wstring m_title; std::wstring m_body; std::filesystem::path m_image; std::wstring m_sound = L"Notification.Default"; std::wstring m_id; std::wstring m_buttons; bool m_silent = false; bool m_textbox = false; SnoreToastActions::Actions m_action = SnoreToastActions::Actions::Clicked; ComPtr<IXmlDocument> m_toastXml; ComPtr<IToastNotificationManagerStatics> m_toastManager; ComPtr<IToastNotifier> m_notifier; ComPtr<IToastNotification> m_notification; ComPtr<ToastEventHandler> m_eventHanlder; static HANDLE ctoastEvent() { static HANDLE _event = [] { std::wstringstream eventName; eventName << L"ToastActivationEvent" << GetCurrentProcessId(); return CreateEvent(nullptr, true, false, eventName.str().c_str()); }(); return _event; } ComPtr<IToastNotificationHistory> getHistory() { ComPtr<IToastNotificationManagerStatics2> toastStatics2; if (ST_CHECK_RESULT(m_toastManager.As(&toastStatics2))) { ComPtr<IToastNotificationHistory> nativeHistory; ST_CHECK_RESULT(toastStatics2->get_History(&nativeHistory)); return nativeHistory; } return {}; } }; SnoreToasts::SnoreToasts(const std::wstring &appID) : d(new SnoreToastsPrivate(this, appID)) { Utils::registerActivator(); } SnoreToasts::~SnoreToasts() { Utils::unregisterActivator(); delete d; } HRESULT SnoreToasts::displayToast(const std::wstring &title, const std::wstring &body, const std::filesystem::path &image) { // asume that we fail d->m_action = SnoreToastActions::Actions::Error; d->m_title = title; d->m_body = body; d->m_image = std::filesystem::absolute(image); if (!d->m_image.empty()) { ST_RETURN_ON_ERROR(d->m_toastManager->GetTemplateContent( ToastTemplateType_ToastImageAndText02, &d->m_toastXml)); } else { ST_RETURN_ON_ERROR(d->m_toastManager->GetTemplateContent(ToastTemplateType_ToastText02, &d->m_toastXml)); } ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeList> rootList; ST_RETURN_ON_ERROR( d->m_toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), &rootList)); ComPtr<IXmlNode> root; ST_RETURN_ON_ERROR(rootList->Item(0, &root)); ComPtr<IXmlNamedNodeMap> rootAttributes; ST_RETURN_ON_ERROR(root->get_Attributes(&rootAttributes)); const auto data = formatAction(SnoreToastActions::Actions::Clicked); ST_RETURN_ON_ERROR(addAttribute(L"launch", rootAttributes.Get(), data)); // Adding buttons if (!d->m_buttons.empty()) { setButtons(root); } else if (d->m_textbox) { setTextBox(root); } ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> audioElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"audio").Get(), &audioElement)); ComPtr<IXmlNode> audioNodeTmp; ST_RETURN_ON_ERROR(audioElement.As(&audioNodeTmp)); ComPtr<IXmlNode> audioNode; ST_RETURN_ON_ERROR(root->AppendChild(audioNodeTmp.Get(), &audioNode)); ComPtr<IXmlNamedNodeMap> attributes; ST_RETURN_ON_ERROR(audioNode->get_Attributes(&attributes)); ST_RETURN_ON_ERROR(addAttribute(L"src", attributes.Get())); ST_RETURN_ON_ERROR(addAttribute(L"silent", attributes.Get())); // printXML(); if (!d->m_image.empty()) { ST_RETURN_ON_ERROR(setImage()); } ST_RETURN_ON_ERROR(setSound()); ST_RETURN_ON_ERROR(setTextValues()); printXML(); ST_RETURN_ON_ERROR(createToast()); d->m_action = SnoreToastActions::Actions::Clicked; return S_OK; } SnoreToastActions::Actions SnoreToasts::userAction() { if (d->m_eventHanlder.Get()) { HANDLE event = d->m_eventHanlder.Get()->event(); if (WaitForSingleObject(event, EVENT_TIMEOUT) == WAIT_TIMEOUT) { d->m_action = SnoreToastActions::Actions::Error; } else { d->m_action = d->m_eventHanlder.Get()->userAction(); } // the initial value is SnoreToastActions::Actions::Hidden so if no action happend when we // end up here, a hide was requested if (d->m_action == SnoreToastActions::Actions::Hidden) { d->m_notifier->Hide(d->m_notification.Get()); tLog << L"The application hid the toast using ToastNotifier.hide()"; } CloseHandle(event); } return d->m_action; } bool SnoreToasts::closeNotification() { std::wstringstream eventName; eventName << L"ToastEvent" << d->m_id; HANDLE event = OpenEventW(EVENT_ALL_ACCESS, FALSE, eventName.str().c_str()); if (event) { SetEvent(event); return true; } if (auto history = d->getHistory()) { if (ST_CHECK_RESULT(history->RemoveGroupedTagWithId( HStringReference(d->m_id.c_str()).Get(), HStringReference(L"SnoreToast").Get(), HStringReference(d->m_appID.c_str()).Get()))) { return true; } } tLog << "Notification " << d->m_id << " does not exist"; return false; } void SnoreToasts::setSound(const std::wstring &soundFile) { d->m_sound = soundFile; } void SnoreToasts::setSilent(bool silent) { d->m_silent = silent; } void SnoreToasts::setId(const std::wstring &id) { if (!id.empty()) { d->m_id = id; } } std::wstring SnoreToasts::id() const { return d->m_id; } void SnoreToasts::setButtons(const std::wstring &buttons) { d->m_buttons = buttons; } void SnoreToasts::setTextBoxEnabled(bool textBoxEnabled) { d->m_textbox = textBoxEnabled; } // Set the value of the "src" attribute of the "image" node HRESULT SnoreToasts::setImage() { ComPtr<IXmlNodeList> nodeList; ST_RETURN_ON_ERROR( d->m_toastXml->GetElementsByTagName(HStringReference(L"image").Get(), &nodeList)); ComPtr<IXmlNode> imageNode; ST_RETURN_ON_ERROR(nodeList->Item(0, &imageNode)); ComPtr<IXmlNamedNodeMap> attributes; ST_RETURN_ON_ERROR(imageNode->get_Attributes(&attributes)); ComPtr<IXmlNode> srcAttribute; ST_RETURN_ON_ERROR(attributes->GetNamedItem(HStringReference(L"src").Get(), &srcAttribute)); return setNodeValueString(HStringReference(d->m_image.wstring().c_str()).Get(), srcAttribute.Get()); } HRESULT SnoreToasts::setSound() { ComPtr<IXmlNodeList> nodeList; ST_RETURN_ON_ERROR( d->m_toastXml->GetElementsByTagName(HStringReference(L"audio").Get(), &nodeList)); ComPtr<IXmlNode> audioNode; ST_RETURN_ON_ERROR(nodeList->Item(0, &audioNode)); ComPtr<IXmlNamedNodeMap> attributes; ST_RETURN_ON_ERROR(audioNode->get_Attributes(&attributes)); ComPtr<IXmlNode> srcAttribute; ST_RETURN_ON_ERROR(attributes->GetNamedItem(HStringReference(L"src").Get(), &srcAttribute)); std::wstring sound; if (d->m_sound.find(L"ms-winsoundevent:") == std::wstring::npos) { sound = L"ms-winsoundevent:"; sound.append(d->m_sound); } else { sound = d->m_sound; } ST_RETURN_ON_ERROR( setNodeValueString(HStringReference(sound.c_str()).Get(), srcAttribute.Get())); ST_RETURN_ON_ERROR(attributes->GetNamedItem(HStringReference(L"silent").Get(), &srcAttribute)); return setNodeValueString(HStringReference(d->m_silent ? L"true" : L"false").Get(), srcAttribute.Get()); } // Set the values of each of the text nodes HRESULT SnoreToasts::setTextValues() { ComPtr<IXmlNodeList> nodeList; ST_RETURN_ON_ERROR( d->m_toastXml->GetElementsByTagName(HStringReference(L"text").Get(), &nodeList)); // create the title ComPtr<IXmlNode> textNode; ST_RETURN_ON_ERROR(nodeList->Item(0, &textNode)); ST_RETURN_ON_ERROR( setNodeValueString(HStringReference(d->m_title.c_str()).Get(), textNode.Get())); ST_RETURN_ON_ERROR(nodeList->Item(1, &textNode)); return setNodeValueString(HStringReference(d->m_body.c_str()).Get(), textNode.Get()); } HRESULT SnoreToasts::setButtons(ComPtr<IXmlNode> root) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionsElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"actions").Get(), &actionsElement)); ComPtr<IXmlNode> actionsNodeTmp; ST_RETURN_ON_ERROR(actionsElement.As(&actionsNodeTmp)); ComPtr<IXmlNode> actionsNode; ST_RETURN_ON_ERROR(root->AppendChild(actionsNodeTmp.Get(), &actionsNode)); std::wstring buttonText; std::wstringstream wss(d->m_buttons); while (std::getline(wss, buttonText, L';')) { ST_RETURN_ON_ERROR(createNewActionButton(actionsNode, buttonText)); } return S_OK; } HRESULT SnoreToasts::setTextBox(ComPtr<IXmlNode> root) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionsElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"actions").Get(), &actionsElement)); ComPtr<IXmlNode> actionsNodeTmp; ST_RETURN_ON_ERROR(actionsElement.As(&actionsNodeTmp)); ComPtr<IXmlNode> actionsNode; ST_RETURN_ON_ERROR(root->AppendChild(actionsNodeTmp.Get(), &actionsNode)); ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> inputElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"input").Get(), &inputElement)); ComPtr<IXmlNode> inputNodeTmp; ST_RETURN_ON_ERROR(inputElement.As(&inputNodeTmp)); ComPtr<IXmlNode> inputNode; ST_RETURN_ON_ERROR(actionsNode->AppendChild(inputNodeTmp.Get(), &inputNode)); ComPtr<IXmlNamedNodeMap> inputAttributes; ST_RETURN_ON_ERROR(inputNode->get_Attributes(&inputAttributes)); ST_RETURN_ON_ERROR(addAttribute(L"id", inputAttributes.Get(), L"textBox")); ST_RETURN_ON_ERROR(addAttribute(L"type", inputAttributes.Get(), L"text")); ST_RETURN_ON_ERROR(addAttribute(L"placeHolderContent", inputAttributes.Get(), L"Type a reply")); ComPtr<IXmlElement> actionElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"action").Get(), &actionElement)); ComPtr<IXmlNode> actionNodeTmp; ST_RETURN_ON_ERROR(actionElement.As(&actionNodeTmp)); ComPtr<IXmlNode> actionNode; ST_RETURN_ON_ERROR(actionsNode->AppendChild(actionNodeTmp.Get(), &actionNode)); ComPtr<IXmlNamedNodeMap> actionAttributes; ST_RETURN_ON_ERROR(actionNode->get_Attributes(&actionAttributes)); ST_RETURN_ON_ERROR(addAttribute(L"content", actionAttributes.Get(), L"Send")); const auto data = formatAction(SnoreToastActions::Actions::ButtonClicked); ST_RETURN_ON_ERROR(addAttribute(L"arguments", actionAttributes.Get(), data)); return addAttribute(L"hint-inputId", actionAttributes.Get(), L"textBox"); } HRESULT SnoreToasts::setEventHandler(ComPtr<IToastNotification> toast) { // Register the event handlers EventRegistrationToken activatedToken, dismissedToken, failedToken; ComPtr<ToastEventHandler> eventHandler(new ToastEventHandler(*this)); ST_RETURN_ON_ERROR(toast->add_Activated(eventHandler.Get(), &activatedToken)); ST_RETURN_ON_ERROR(toast->add_Dismissed(eventHandler.Get(), &dismissedToken)); ST_RETURN_ON_ERROR(toast->add_Failed(eventHandler.Get(), &failedToken)); d->m_eventHanlder = eventHandler; return S_OK; } HRESULT SnoreToasts::setNodeValueString(const HSTRING &inputString, IXmlNode *node) { ComPtr<IXmlText> inputText; ST_RETURN_ON_ERROR(d->m_toastXml->CreateTextNode(inputString, &inputText)); ComPtr<IXmlNode> inputTextNode; ST_RETURN_ON_ERROR(inputText.As(&inputTextNode)); ComPtr<IXmlNode> pAppendedChild; return node->AppendChild(inputTextNode.Get(), &pAppendedChild); } HRESULT SnoreToasts::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlAttribute> srcAttribute; HRESULT hr = d->m_toastXml->CreateAttribute(HStringReference(name.c_str()).Get(), &srcAttribute); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> node; hr = srcAttribute.As(&node); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> pNode; hr = attributeMap->SetNamedItem(node.Get(), &pNode); } } return hr; } HRESULT SnoreToasts::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap, const std::wstring &value) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlAttribute> srcAttribute; ST_RETURN_ON_ERROR( d->m_toastXml->CreateAttribute(HStringReference(name.c_str()).Get(), &srcAttribute)); ComPtr<IXmlNode> node; ST_RETURN_ON_ERROR(srcAttribute.As(&node)); ComPtr<IXmlNode> pNode; ST_RETURN_ON_ERROR(attributeMap->SetNamedItem(node.Get(), &pNode)); return setNodeValueString(HStringReference(value.c_str()).Get(), node.Get()); } HRESULT SnoreToasts::createNewActionButton(ComPtr<IXmlNode> actionsNode, const std::wstring &value) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionElement; ST_RETURN_ON_ERROR( d->m_toastXml->CreateElement(HStringReference(L"action").Get(), &actionElement)); ComPtr<IXmlNode> actionNodeTmp; ST_RETURN_ON_ERROR(actionElement.As(&actionNodeTmp)); ComPtr<IXmlNode> actionNode; ST_RETURN_ON_ERROR(actionsNode->AppendChild(actionNodeTmp.Get(), &actionNode)); ComPtr<IXmlNamedNodeMap> actionAttributes; ST_RETURN_ON_ERROR(actionNode->get_Attributes(&actionAttributes)); ST_RETURN_ON_ERROR(addAttribute(L"content", actionAttributes.Get(), value)); const auto data = formatAction(SnoreToastActions::Actions::ButtonClicked, { { L"button", value } }); ST_RETURN_ON_ERROR(addAttribute(L"arguments", actionAttributes.Get(), data)); return addAttribute(L"activationType", actionAttributes.Get(), L"foreground"); } void SnoreToasts::printXML() { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeSerializer> s; ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> ss(d->m_toastXml); ss.As(&s); HSTRING string; s->GetXml(&string); PCWSTR str = WindowsGetStringRawBuffer(string, nullptr); tLog << L"------------------------\n\t\t\t" << str << L"\n\t\t" << L"------------------------"; } std::filesystem::path SnoreToasts::pipeName() const { return d->m_pipeName; } void SnoreToasts::setPipeName(const std::filesystem::path &pipeName) { d->m_pipeName = pipeName; } std::filesystem::path SnoreToasts::application() const { return d->m_application; } void SnoreToasts::setApplication(const std::filesystem::path &application) { d->m_application = application; } std::wstring SnoreToasts::formatAction( const SnoreToastActions::Actions &action, const std::vector<std::pair<std::wstring_view, std::wstring_view>> &extraData) const { const auto pipe = d->m_pipeName.wstring(); const auto application = d->m_application.wstring(); std::vector<std::pair<std::wstring_view, std::wstring_view>> data = { { L"action", SnoreToastActions::getActionString(action) }, { L"notificationId", std::wstring_view(d->m_id) }, { L"pipe", std::wstring_view(pipe) }, { L"application", std::wstring_view(application) } }; data.insert(data.end(), extraData.cbegin(), extraData.cend()); return Utils::formatData(data); } // Create and display the toast HRESULT SnoreToasts::createToast() { ST_RETURN_ON_ERROR(d->m_toastManager->CreateToastNotifierWithId( HStringReference(d->m_appID.c_str()).Get(), &d->m_notifier)); ComPtr<IToastNotificationFactory> factory; ST_RETURN_ON_ERROR(GetActivationFactory( HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), &factory)); ST_RETURN_ON_ERROR(factory->CreateToastNotification(d->m_toastXml.Get(), &d->m_notification)); ComPtr<Notifications::IToastNotification2> toastV2; if (SUCCEEDED(d->m_notification.As(&toastV2))) { ST_RETURN_ON_ERROR(toastV2->put_Tag(HStringReference(d->m_id.c_str()).Get())); ST_RETURN_ON_ERROR(toastV2->put_Group(HStringReference(L"SnoreToast").Get())); } std::wstring error; NotificationSetting setting = NotificationSetting_Enabled; if (!ST_CHECK_RESULT(d->m_notifier->get_Setting(&setting))) { tLog << "Failed to retreive NotificationSettings ensure your appId is registered"; } switch (setting) { case NotificationSetting_Enabled: ST_RETURN_ON_ERROR(setEventHandler(d->m_notification)); return d->m_notifier->Show(d->m_notification.Get()); case NotificationSetting_DisabledForApplication: error = L"DisabledForApplication"; break; case NotificationSetting_DisabledForUser: error = L"DisabledForUser"; break; case NotificationSetting_DisabledByGroupPolicy: error = L"DisabledByGroupPolicy"; break; case NotificationSetting_DisabledByManifest: error = L"DisabledByManifest"; break; } std::wstringstream err; err << L"Notifications are disabled\n" << L"Reason: " << error << L"Please make sure that the app id is set correctly.\n" << L"Command Line: " << GetCommandLineW(); tLog << err.str(); std::wcerr << err.str() << std::endl; return S_FALSE; } std::wstring SnoreToasts::version() { static std::wstring ver = [] { std::wstringstream st; st << SNORETOAST_VERSION_MAJOR << L"." << SNORETOAST_VERSION_MINOR << L"." << SNORETOAST_VERSION_PATCH; return st.str(); }(); return ver; } HRESULT SnoreToasts::backgroundCallback(const std::wstring &appUserModelId, const std::wstring &invokedArgs, const std::wstring &msg) { tLog << "CToastNotificationActivationCallback::Activate: " << appUserModelId << " : " << invokedArgs << " : " << msg; const auto dataMap = Utils::splitData(invokedArgs); const auto action = SnoreToastActions::getAction(dataMap.at(L"action")); std::wstring dataString; if (action == SnoreToastActions::Actions::TextEntered) { std::wstringstream sMsg; sMsg << invokedArgs << L"text=" << msg; dataString = sMsg.str(); } else { dataString = invokedArgs; } const auto pipe = dataMap.find(L"pipe"); if (pipe != dataMap.cend()) { if (!Utils::writePipe(pipe->second, dataString)) { const auto app = dataMap.find(L"application"); if (app != dataMap.cend()) { if (Utils::startProcess(app->second)) { Utils::writePipe(pipe->second, dataString, true); } } } } tLog << dataString; if (!SetEvent(SnoreToastsPrivate::ctoastEvent())) { tLog << "SetEvent failed" << GetLastError(); return S_FALSE; } return S_OK; } void SnoreToasts::waitForCallbackActivation() { Utils::registerActivator(); WaitForSingleObject(SnoreToastsPrivate::ctoastEvent(), EVENT_TIMEOUT); Utils::unregisterActivator(); }