tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! Windows:ToastNotificationManager(windows crate 的 WinRT 接口)。
//!
//! - 替换:tag(id 的散列)+ group(会话的散列)相同的新通知替换旧的。
//! - 点击和按钮:运行中经 Activated 事件交回,参数是编码后的内容;应用退出后点击,登记了激活方式(COM 激活器或 URI 协议)的
//!   安装包以启动参数带来同样的内容(mod.rs 的 decode_arg),没有登记的只是启动应用。
//! - AUMID:用应用的 identifier(NSIS、MSI 安装包在开始菜单快捷方式上登记)。开发时直接运行的构建没有登记,通知显示不出来,
//!   改用 PowerShell 的 AUMID:能显示(显示为 PowerShell 发出),但点击可能回不到应用。

use std::collections::HashMap;
use std::sync::Mutex;

use ::windows::core::{IInspectable, Interface, HSTRING};
use ::windows::Data::Xml::Dom::XmlDocument;
use ::windows::Foundation::TypedEventHandler;
use ::windows::UI::Notifications::{NotificationSetting, ToastActivatedEventArgs, ToastFailedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier};
use ::windows::Win32::Foundation::ERROR_SUCCESS;
use ::windows::Win32::System::Registry::{RegCloseKey, RegOpenKeyExW, HKEY, HKEY_CURRENT_USER, KEY_READ};
use tauri::{AppHandle, Runtime};

use super::{toast, Clear, Payload, Request, Sink};
use crate::error::{Error, Result};

pub(super) struct Backend {
    aumid: HSTRING,
    sink: Sink,
    /// 显示过的通知:保留对象,Activated 事件才会一直有效;清除时要用到 tag 和 group。
    shown: Mutex<HashMap<String, Shown>>,
}

struct Shown {
    _toast: ToastNotification,
    tag: HSTRING,
    group: HSTRING,
    conversation_id: Option<String>,
}

fn aumid_registered(identifier: &str) -> bool {
    let key = HSTRING::from(format!("Software\\Classes\\AppUserModelId\\{identifier}"));
    let mut handle = HKEY::default();
    // SAFETY:只打开、随即关闭
    unsafe {
        if RegOpenKeyExW(HKEY_CURRENT_USER, &key, None, KEY_READ, &mut handle) == ERROR_SUCCESS {
            let _ = RegCloseKey(handle);
            return true;
        }
    }
    false
}

impl Backend {
    pub fn new<R: Runtime>(_app: &AppHandle<R>, identifier: &str, sink: Sink) -> Self {
        let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_string_lossy().into_owned())).unwrap_or_default();
        let dev = toast::is_dev_build(&exe_dir);
        let aumid = toast::pick_aumid(identifier, aumid_registered(identifier), dev);
        if aumid != identifier {
            log::warn!("开发构建没有登记 AUMID,通知借用 PowerShell 的 AUMID 显示,点击可能回不到应用;用安装包安装后正常");
        }
        Self { aumid: HSTRING::from(aumid), sink, shown: Mutex::new(HashMap::new()) }
    }

    fn notifier(&self) -> ::windows::core::Result<ToastNotifier> {
        ToastNotificationManager::CreateToastNotifierWithId(&self.aumid)
    }

    pub async fn show(&self, req: &Request, payload: &Payload) -> Result<()> {
        self.show_now(req, payload)
    }

    fn show_now(&self, req: &Request, payload: &Payload) -> Result<()> {
        let notifier = self.notifier().map_err(win_err)?;
        // 用户或组策略关闭了本应用的通知
        if let Ok(setting) = notifier.Setting() {
            if setting != NotificationSetting::Enabled {
                return Err(Error::permission_denied("notification_disabled"));
            }
        }
        let doc = XmlDocument::new().map_err(win_err)?;
        doc.LoadXml(&HSTRING::from(toast::xml(req, payload))).map_err(win_err)?;
        let toast_obj = ToastNotification::CreateToastNotification(&doc).map_err(win_err)?;
        let tag = HSTRING::from(toast::tag(&req.id));
        let group = HSTRING::from(toast::group(req.group_key()));
        toast_obj.SetTag(&tag).map_err(win_err)?;
        toast_obj.SetGroup(&group).map_err(win_err)?;
        let sink = self.sink.clone();
        toast_obj
            .Activated(&TypedEventHandler::<ToastNotification, IInspectable>::new(move |_, args| {
                let Some(args) = args.as_ref() else { return Ok(()) };
                let Ok(args) = args.cast::<ToastActivatedEventArgs>() else { return Ok(()) };
                if let Some(payload) = super::decode_arg(&args.Arguments()?.to_string()) {
                    sink(payload, false);
                }
                Ok(())
            }))
            .map_err(win_err)?;
        let id = req.id.clone();
        toast_obj
            .Failed(&TypedEventHandler::<ToastNotification, ToastFailedEventArgs>::new(move |_, args| {
                if let Some(args) = args.as_ref() {
                    log::warn!("通知 {id} 显示失败:{:?}", args.ErrorCode());
                }
                Ok(())
            }))
            .map_err(win_err)?;
        notifier.Show(&toast_obj).map_err(win_err)?;
        self.shown.lock().unwrap().insert(req.id.clone(), Shown { _toast: toast_obj, tag, group, conversation_id: req.conversation_id.clone() });
        Ok(())
    }

    pub async fn clear(&self, target: &Clear) -> Result<()> {
        let history = ToastNotificationManager::History().map_err(win_err)?;
        let mut shown = self.shown.lock().unwrap();
        match target {
            Clear::Id(id) => {
                if let Some(entry) = shown.remove(id) {
                    let _ = history.RemoveGroupedTagWithId(&entry.tag, &entry.group, &self.aumid);
                }
            }
            Clear::Conversation(cid) => {
                let ids: Vec<String> = shown.iter().filter(|(_, s)| s.conversation_id.as_deref() == Some(cid.as_str())).map(|(id, _)| id.clone()).collect();
                for id in ids {
                    if let Some(entry) = shown.remove(&id) {
                        let _ = history.RemoveGroupedTagWithId(&entry.tag, &entry.group, &self.aumid);
                    }
                }
                // 上次运行时显示、还留在操作中心里的(按会话归组的)
                let _ = history.RemoveGroupWithId(&HSTRING::from(toast::group(cid)), &self.aumid);
            }
            Clear::All => {
                shown.clear();
                history.ClearWithId(&self.aumid).map_err(win_err)?;
            }
        }
        Ok(())
    }

    pub async fn permission(&self) -> &'static str {
        match self.notifier().and_then(|n| n.Setting()) {
            Ok(NotificationSetting::Enabled) => "granted",
            Ok(_) => "denied",
            Err(_) => "default",
        }
    }

    /// Windows 没有请求权限的对话框:只读取当前设置。
    pub async fn request_permission(&self) -> &'static str {
        if self.permission().await == "denied" {
            "denied"
        } else {
            "granted"
        }
    }
}

fn win_err(err: ::windows::core::Error) -> Error {
    Error::internal(format!("Windows 通知:{err}"))
}