@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +556 -0
- package/INSTALLATION.md +310 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/client.js +3 -0
- package/components.json +20 -0
- package/cordis.patch.yml +3 -0
- package/docs/README.en.md +196 -0
- package/docs/screenshots/en/overview.png +0 -0
- package/docs/screenshots/en/question.png +0 -0
- package/docs/screenshots/zh/overview.png +0 -0
- package/docs/screenshots/zh/question.png +0 -0
- package/icon.svg +24 -0
- package/index.js +2 -0
- package/lib/account.js +1 -0
- package/lib/app-mapping.js +1 -0
- package/lib/i18n.js +1 -0
- package/lib/media-enrich.js +1 -0
- package/lib/notifications.js +1 -0
- package/lib/performance.js +1 -0
- package/lib/platform/index.js +1 -0
- package/lib/probe-adapter.js +1 -0
- package/lib/probe-daemon.js +1 -0
- package/lib/probe.js +1 -0
- package/lib/qr.js +1 -0
- package/lib/runtime.js +1 -0
- package/lib/service.js +1 -0
- package/lib/sessions.js +1 -0
- package/lib/settings.js +1 -0
- package/lib/types.js +1 -0
- package/lib/util.js +1 -0
- package/lib/weather.js +1 -0
- package/lib/ws.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +118 -4
- package/scripts/browser-shot.mjs +217 -0
- package/scripts/build-dsh.mjs +228 -0
- package/scripts/build-rust.mjs +319 -0
- package/scripts/build-web.mjs +17 -0
- package/scripts/check-npm-package.mjs +322 -0
- package/scripts/mock-preview.py +574 -0
- package/scripts/screenshot-compose.py +89 -0
- package/scripts/screenshot-device.sh +75 -0
- package/src/dsh/client/api.ts +68 -0
- package/src/dsh/client/i18n.ts +573 -0
- package/src/dsh/client/icons.ts +56 -0
- package/src/dsh/client/index.ts +95 -0
- package/src/dsh/client/logic.ts +310 -0
- package/src/dsh/client/panel.ts +483 -0
- package/src/dsh/client/sections/apps.ts +40 -0
- package/src/dsh/client/sections/balance.ts +38 -0
- package/src/dsh/client/sections/connection.ts +97 -0
- package/src/dsh/client/sections/notifications.ts +43 -0
- package/src/dsh/client/sections/pomodoro.ts +184 -0
- package/src/dsh/client/sections/preferences.ts +138 -0
- package/src/dsh/client/sections/system.ts +120 -0
- package/src/dsh/client/sections/weather.ts +276 -0
- package/src/dsh/client/styles.ts +1119 -0
- package/src/dsh/client/types.ts +366 -0
- package/src/dsh/client/ui.ts +142 -0
- package/src/dsh/index.ts +108 -0
- package/src/dsh/lib/account.ts +520 -0
- package/src/dsh/lib/app-mapping.ts +366 -0
- package/src/dsh/lib/i18n.ts +82 -0
- package/src/dsh/lib/media-enrich.ts +202 -0
- package/src/dsh/lib/notifications.ts +322 -0
- package/src/dsh/lib/performance.ts +279 -0
- package/src/dsh/lib/platform/index.ts +43 -0
- package/src/dsh/lib/probe-adapter.ts +275 -0
- package/src/dsh/lib/probe-daemon.ts +359 -0
- package/src/dsh/lib/probe.ts +395 -0
- package/src/dsh/lib/qr.ts +42 -0
- package/src/dsh/lib/runtime.ts +126 -0
- package/src/dsh/lib/service.ts +1645 -0
- package/src/dsh/lib/sessions.ts +907 -0
- package/src/dsh/lib/settings.ts +468 -0
- package/src/dsh/lib/types.ts +231 -0
- package/src/dsh/lib/util.ts +115 -0
- package/src/dsh/lib/weather.ts +532 -0
- package/src/dsh/lib/ws.ts +328 -0
- package/src/dsh/types/qrcode-generator.d.ts +16 -0
- package/src/rust/Cargo.lock +1431 -0
- package/src/rust/Cargo.toml +68 -0
- package/src/rust/src/cli.rs +233 -0
- package/src/rust/src/daemon.rs +140 -0
- package/src/rust/src/location/linux.rs +118 -0
- package/src/rust/src/location/macos.rs +26 -0
- package/src/rust/src/location/mod.rs +170 -0
- package/src/rust/src/location/windows.rs +205 -0
- package/src/rust/src/main.rs +42 -0
- package/src/rust/src/media/catalog.rs +454 -0
- package/src/rust/src/media/linux.rs +350 -0
- package/src/rust/src/media/macos/accessibility.rs +59 -0
- package/src/rust/src/media/macos.rs +366 -0
- package/src/rust/src/media/mod.rs +298 -0
- package/src/rust/src/media/vendor/apple_music.rs +90 -0
- package/src/rust/src/media/vendor/common.rs +186 -0
- package/src/rust/src/media/vendor/kugou.rs +52 -0
- package/src/rust/src/media/vendor/kuwo.rs +49 -0
- package/src/rust/src/media/vendor/mod.rs +75 -0
- package/src/rust/src/media/vendor/netease.rs +1715 -0
- package/src/rust/src/media/vendor/qishui.rs +36 -0
- package/src/rust/src/media/vendor/qqmusic.rs +33 -0
- package/src/rust/src/media/vendor/spotify.rs +46 -0
- package/src/rust/src/media/vendor/wesing.rs +39 -0
- package/src/rust/src/media/vendor/youtube_music.rs +49 -0
- package/src/rust/src/media/windows/accessibility.rs +85 -0
- package/src/rust/src/media/windows/audio.rs +116 -0
- package/src/rust/src/media/windows/private.rs +291 -0
- package/src/rust/src/media/windows.rs +775 -0
- package/src/rust/src/notifications/linux.rs +276 -0
- package/src/rust/src/notifications/macos.rs +382 -0
- package/src/rust/src/notifications/mod.rs +244 -0
- package/src/rust/src/notifications/windows.rs +565 -0
- package/src/rust/src/perf/common.rs +109 -0
- package/src/rust/src/perf/linux.rs +282 -0
- package/src/rust/src/perf/macos.rs +28 -0
- package/src/rust/src/perf/mod.rs +157 -0
- package/src/rust/src/perf/windows.rs +981 -0
- package/src/rust/src/util.rs +360 -0
- package/src/rust/src/volume/linux.rs +195 -0
- package/src/rust/src/volume/macos.rs +54 -0
- package/src/rust/src/volume/mod.rs +116 -0
- package/src/rust/src/volume/windows.rs +75 -0
- package/src/rust/tests/cli.rs +79 -0
- package/src/rust/tests/daemon.rs +107 -0
- package/src/svelte/App.svelte +385 -0
- package/src/svelte/index.html +18 -0
- package/src/svelte/lib/billing.svelte.ts +228 -0
- package/src/svelte/lib/components/BalanceCard.svelte +173 -0
- package/src/svelte/lib/components/ClockCard.svelte +183 -0
- package/src/svelte/lib/components/CountdownCard.svelte +261 -0
- package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
- package/src/svelte/lib/components/EmptyState.svelte +34 -0
- package/src/svelte/lib/components/GpuCard.svelte +245 -0
- package/src/svelte/lib/components/HomePage.svelte +34 -0
- package/src/svelte/lib/components/MusicControls.svelte +256 -0
- package/src/svelte/lib/components/MusicCover.svelte +88 -0
- package/src/svelte/lib/components/MusicPage.svelte +116 -0
- package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
- package/src/svelte/lib/components/Panel.svelte +22 -0
- package/src/svelte/lib/components/PerformancePage.svelte +55 -0
- package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
- package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
- package/src/svelte/lib/components/RadialGauge.svelte +99 -0
- package/src/svelte/lib/components/Rail.svelte +217 -0
- package/src/svelte/lib/components/SectionHeader.svelte +40 -0
- package/src/svelte/lib/components/SeekBar.svelte +421 -0
- package/src/svelte/lib/components/SessionList.svelte +364 -0
- package/src/svelte/lib/components/SessionStats.svelte +98 -0
- package/src/svelte/lib/components/SessionsPage.svelte +61 -0
- package/src/svelte/lib/components/SpendCard.svelte +105 -0
- package/src/svelte/lib/components/ThermalCard.svelte +249 -0
- package/src/svelte/lib/components/Toast.svelte +32 -0
- package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
- package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
- package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
- package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
- package/src/svelte/lib/components/ui/alert/index.ts +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
- package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
- package/src/svelte/lib/components/ui/badge/index.ts +2 -0
- package/src/svelte/lib/components/ui/button/button.svelte +82 -0
- package/src/svelte/lib/components/ui/button/index.ts +17 -0
- package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card.svelte +22 -0
- package/src/svelte/lib/components/ui/card/index.ts +25 -0
- package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
- package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
- package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/index.ts +22 -0
- package/src/svelte/lib/components/ui/input/index.ts +7 -0
- package/src/svelte/lib/components/ui/input/input.svelte +48 -0
- package/src/svelte/lib/components/ui/progress/index.ts +7 -0
- package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
- package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
- package/src/svelte/lib/components/ui/separator/index.ts +7 -0
- package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
- package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
- package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
- package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
- package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/src/svelte/lib/components/ui/slider/index.ts +7 -0
- package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
- package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/svelte/lib/countdown.svelte.ts +252 -0
- package/src/svelte/lib/format.ts +199 -0
- package/src/svelte/lib/i18n.svelte.ts +583 -0
- package/src/svelte/lib/media.ts +32 -0
- package/src/svelte/lib/pages.ts +18 -0
- package/src/svelte/lib/pomodoro.svelte.ts +223 -0
- package/src/svelte/lib/session-tree.ts +80 -0
- package/src/svelte/lib/state.svelte.ts +359 -0
- package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
- package/src/svelte/lib/types.ts +306 -0
- package/src/svelte/lib/utils.ts +8 -0
- package/src/svelte/lib/weather.ts +84 -0
- package/src/svelte/lib/ws.ts +104 -0
- package/src/svelte/main.ts +13 -0
- package/src/svelte/styles/global.css +315 -0
- package/src/svelte/vite-env.d.ts +12 -0
- package/static/fonts/LICENSE.txt +287 -0
- package/static/icon.svg +24 -0
- package/static/manifest.webmanifest +18 -0
- package/svelte.config.js +5 -0
- package/tsconfig.dsh-client.json +18 -0
- package/tsconfig.dsh.json +21 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +31 -0
- package/web/assets/index-DLdSfnJ3.css +1 -0
- package/web/assets/index-YNE8X0Uy.js +6 -0
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
- package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
- package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
- package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
- package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
- package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
- package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
- package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
- package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
- package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
- package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
- package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
- package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
- package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
- package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
- package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
- package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
- package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
- package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
- package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
- package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
- package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
- package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
- package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
- package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
- package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
- package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
- package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
- package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
- package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
- package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
- package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
- package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
- package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
- package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
- package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
- package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
- package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
- package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
- package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
- package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
- package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
- package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
- package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
- package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
- package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
- package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
- package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
- package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
- package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
- package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
- package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
- package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
- package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
- package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
- package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
- package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
- package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
- package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
- package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
- package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
- package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
- package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
- package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
- package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
- package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
- package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
- package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
- package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
- package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
- package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
- package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
- package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
- package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
- package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
- package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
- package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
- package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
- package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
- package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
- package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
- package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
- package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
- package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
- package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
- package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
- package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
- package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
- package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
- package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
- package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
- package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
- package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
- package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
- package/web/fonts/LICENSE.txt +287 -0
- package/web/icon.svg +24 -0
- package/web/index.html +19 -0
- package/web/manifest.webmanifest +18 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//! Linux notification probe: watch `org.freedesktop.Notifications`.
|
|
2
|
+
//!
|
|
3
|
+
//! `Notify` is a method call to the desktop notification daemon, so a normal
|
|
4
|
+
//! D-Bus client cannot observe it. The probe turns its connection into a bus
|
|
5
|
+
//! monitor with `org.freedesktop.DBus.Monitoring.BecomeMonitor` (the same
|
|
6
|
+
//! mechanism `dbus-monitor` uses) and filters for `Notify` calls.
|
|
7
|
+
//!
|
|
8
|
+
//! `notifications watch` streams one JSON snapshot per notification, which is
|
|
9
|
+
//! what the Node service consumes. `notifications` samples a short window and
|
|
10
|
+
//! returns whatever arrived in it (there is no portable history API).
|
|
11
|
+
|
|
12
|
+
use std::collections::HashMap;
|
|
13
|
+
use std::io::Write;
|
|
14
|
+
use std::path::PathBuf;
|
|
15
|
+
use std::sync::mpsc::{channel, Receiver};
|
|
16
|
+
use std::time::{Duration, Instant};
|
|
17
|
+
|
|
18
|
+
use zbus::blocking::{Connection, MessageIterator};
|
|
19
|
+
use zbus::message::Message;
|
|
20
|
+
use zbus::zvariant::OwnedValue;
|
|
21
|
+
|
|
22
|
+
use super::{snapshot_json, NotificationItem, Snapshot, SnapshotResult};
|
|
23
|
+
use crate::cli::CliResult;
|
|
24
|
+
use crate::util;
|
|
25
|
+
|
|
26
|
+
pub const SOURCE: &str = "linux-dbus-notifications";
|
|
27
|
+
pub const LIVE_ONLY: bool = true;
|
|
28
|
+
|
|
29
|
+
const SNAPSHOT_WINDOW_MS: u64 = 1200;
|
|
30
|
+
const MAX_ICON_BYTES: u64 = 512 * 1024;
|
|
31
|
+
const NOTIFY_RULE: &str =
|
|
32
|
+
"type='method_call',interface='org.freedesktop.Notifications',member='Notify'";
|
|
33
|
+
|
|
34
|
+
/// D-Bus `Notify` signature: susssasa{sv}i.
|
|
35
|
+
type NotifyBody = (
|
|
36
|
+
String,
|
|
37
|
+
u32,
|
|
38
|
+
String,
|
|
39
|
+
String,
|
|
40
|
+
String,
|
|
41
|
+
Vec<String>,
|
|
42
|
+
HashMap<String, OwnedValue>,
|
|
43
|
+
i32,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
fn monitor() -> Result<Receiver<Message>, String> {
|
|
47
|
+
let connection =
|
|
48
|
+
Connection::session().map_err(|error| format!("D-Bus session unavailable: {error}"))?;
|
|
49
|
+
connection
|
|
50
|
+
.call_method(
|
|
51
|
+
Some("org.freedesktop.DBus"),
|
|
52
|
+
"/org/freedesktop/DBus",
|
|
53
|
+
Some("org.freedesktop.DBus.Monitoring"),
|
|
54
|
+
"BecomeMonitor",
|
|
55
|
+
&(&[NOTIFY_RULE] as &[&str], 0u32),
|
|
56
|
+
)
|
|
57
|
+
.map_err(|error| format!("could not monitor org.freedesktop.Notifications: {error}"))?;
|
|
58
|
+
|
|
59
|
+
let (sender, receiver) = channel();
|
|
60
|
+
std::thread::spawn(move || {
|
|
61
|
+
let iterator = MessageIterator::from(connection);
|
|
62
|
+
for message in iterator {
|
|
63
|
+
let Ok(message) = message else {
|
|
64
|
+
break;
|
|
65
|
+
};
|
|
66
|
+
if sender.send(message).is_err() {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
Ok(receiver)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn read_icon_source(source: &str) -> Option<String> {
|
|
75
|
+
let source = source.trim();
|
|
76
|
+
if source.is_empty() {
|
|
77
|
+
return None;
|
|
78
|
+
}
|
|
79
|
+
let path: PathBuf = if source.starts_with("file://") {
|
|
80
|
+
util::file_url_to_path(source)?
|
|
81
|
+
} else {
|
|
82
|
+
PathBuf::from(source)
|
|
83
|
+
};
|
|
84
|
+
if !path.is_absolute() {
|
|
85
|
+
// A themed icon name (for example "firefox") is not a file path.
|
|
86
|
+
return None;
|
|
87
|
+
}
|
|
88
|
+
util::file_image_data_url(&path, MAX_ICON_BYTES)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn hint_string(hints: &HashMap<String, OwnedValue>, key: &str) -> Option<String> {
|
|
92
|
+
let value = hints.get(key)?.try_clone().ok()?;
|
|
93
|
+
String::try_from(value).ok()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn hint_icon(hints: &HashMap<String, OwnedValue>) -> Option<String> {
|
|
97
|
+
for key in ["image-path", "image_path", "imagePath"] {
|
|
98
|
+
if let Some(source) = hint_string(hints, key) {
|
|
99
|
+
if let Some(icon) = read_icon_source(&source) {
|
|
100
|
+
return Some(icon);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
None
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn item_from_message(message: &Message, counter: u64) -> Option<NotificationItem> {
|
|
108
|
+
let header = message.header();
|
|
109
|
+
if header.member().map(|member| member.as_str()) != Some("Notify") {
|
|
110
|
+
return None;
|
|
111
|
+
}
|
|
112
|
+
let body: NotifyBody = message.body().deserialize().ok()?;
|
|
113
|
+
let (app_name, _replaces_id, app_icon, summary, body_text, _actions, hints, _timeout) = body;
|
|
114
|
+
let app_name = app_name.trim();
|
|
115
|
+
let app_name = if app_name.is_empty() {
|
|
116
|
+
"通知".to_string()
|
|
117
|
+
} else {
|
|
118
|
+
app_name.to_string()
|
|
119
|
+
};
|
|
120
|
+
let title = if summary.trim().is_empty() {
|
|
121
|
+
app_name.clone()
|
|
122
|
+
} else {
|
|
123
|
+
summary.trim().to_string()
|
|
124
|
+
};
|
|
125
|
+
let body_text = body_text.trim().to_string();
|
|
126
|
+
let avatar = read_icon_source(&app_icon).or_else(|| hint_icon(&hints));
|
|
127
|
+
let has_image = avatar.is_some()
|
|
128
|
+
|| hints.contains_key("image-data")
|
|
129
|
+
|| hints.contains_key("image_data")
|
|
130
|
+
|| hints.contains_key("image-path")
|
|
131
|
+
|| hints.contains_key("image_path");
|
|
132
|
+
let received_at = util::now_ms();
|
|
133
|
+
Some(NotificationItem {
|
|
134
|
+
id: format!("{received_at}-{counter}"),
|
|
135
|
+
app_key: app_name.clone(),
|
|
136
|
+
app_name,
|
|
137
|
+
title: super::truncate(&title, 160),
|
|
138
|
+
body: super::truncate(&body_text, 260),
|
|
139
|
+
avatar,
|
|
140
|
+
has_image,
|
|
141
|
+
received_at,
|
|
142
|
+
is_group: false,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn push_item(items: &mut Vec<NotificationItem>, item: NotificationItem, limit: usize) {
|
|
147
|
+
items.insert(0, item);
|
|
148
|
+
items.truncate(limit);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Sample live notifications for a short window.
|
|
152
|
+
pub fn read(limit: usize) -> SnapshotResult {
|
|
153
|
+
let receiver = monitor()?;
|
|
154
|
+
let deadline = Instant::now() + Duration::from_millis(SNAPSHOT_WINDOW_MS);
|
|
155
|
+
let mut items = Vec::new();
|
|
156
|
+
let mut counter = 0_u64;
|
|
157
|
+
loop {
|
|
158
|
+
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
159
|
+
if remaining.is_zero() {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
match receiver.recv_timeout(remaining) {
|
|
163
|
+
Ok(message) => {
|
|
164
|
+
if let Some(item) = item_from_message(&message, counter) {
|
|
165
|
+
counter += 1;
|
|
166
|
+
push_item(&mut items, item, limit);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Err(_) => break,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
Ok(Snapshot {
|
|
173
|
+
source: SOURCE,
|
|
174
|
+
live_only: LIVE_ONLY,
|
|
175
|
+
notifications: items,
|
|
176
|
+
updated_at: util::now_ms(),
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn print_json(value: &serde_json::Value) {
|
|
181
|
+
match serde_json::to_string(value) {
|
|
182
|
+
Ok(line) => {
|
|
183
|
+
println!("{line}");
|
|
184
|
+
let _ = std::io::stdout().flush();
|
|
185
|
+
}
|
|
186
|
+
Err(error) => {
|
|
187
|
+
eprintln!("could not encode notification snapshot: {error}");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn print_snapshot(snapshot: &Snapshot, limit: usize) {
|
|
193
|
+
print_json(&snapshot_json(snapshot, limit, util::now_ms()));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Print the notification-shaped error and exit; the Node service reads the
|
|
197
|
+
/// error from the stream instead of the generic CLI error envelope.
|
|
198
|
+
fn fail(error: &str, limit: usize) -> ! {
|
|
199
|
+
print_json(&super::error_json(
|
|
200
|
+
error,
|
|
201
|
+
SOURCE,
|
|
202
|
+
LIVE_ONLY,
|
|
203
|
+
limit,
|
|
204
|
+
util::now_ms(),
|
|
205
|
+
));
|
|
206
|
+
std::process::exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Stream snapshots until the monitor connection closes.
|
|
210
|
+
pub fn watch(limit: usize) -> CliResult {
|
|
211
|
+
let receiver = match monitor() {
|
|
212
|
+
Ok(receiver) => receiver,
|
|
213
|
+
Err(error) => fail(&error, limit),
|
|
214
|
+
};
|
|
215
|
+
let mut items: Vec<NotificationItem> = Vec::new();
|
|
216
|
+
let mut counter = 0_u64;
|
|
217
|
+
print_snapshot(
|
|
218
|
+
&Snapshot {
|
|
219
|
+
source: SOURCE,
|
|
220
|
+
live_only: LIVE_ONLY,
|
|
221
|
+
notifications: Vec::new(),
|
|
222
|
+
updated_at: util::now_ms(),
|
|
223
|
+
},
|
|
224
|
+
limit,
|
|
225
|
+
);
|
|
226
|
+
for message in receiver {
|
|
227
|
+
if let Some(item) = item_from_message(&message, counter) {
|
|
228
|
+
counter += 1;
|
|
229
|
+
push_item(&mut items, item, limit);
|
|
230
|
+
print_snapshot(
|
|
231
|
+
&Snapshot {
|
|
232
|
+
source: SOURCE,
|
|
233
|
+
live_only: LIVE_ONLY,
|
|
234
|
+
notifications: items.clone(),
|
|
235
|
+
updated_at: util::now_ms(),
|
|
236
|
+
},
|
|
237
|
+
limit,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
fail("notification monitor ended (D-Bus session closed)", limit)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[cfg(test)]
|
|
245
|
+
mod tests {
|
|
246
|
+
use super::*;
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn ignores_themed_icon_names_and_missing_files() {
|
|
250
|
+
assert!(read_icon_source("firefox").is_none());
|
|
251
|
+
assert!(read_icon_source("/nonexistent/dsh-probe/icon.png").is_none());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn keeps_only_the_most_recent_items() {
|
|
256
|
+
let mut items = Vec::new();
|
|
257
|
+
for index in 0..3 {
|
|
258
|
+
push_item(
|
|
259
|
+
&mut items,
|
|
260
|
+
NotificationItem {
|
|
261
|
+
id: index.to_string(),
|
|
262
|
+
..Default::default()
|
|
263
|
+
},
|
|
264
|
+
2,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
assert_eq!(items.len(), 2);
|
|
268
|
+
assert_eq!(items[0].id, "2");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn notification_rule_targets_notify_calls() {
|
|
273
|
+
assert!(NOTIFY_RULE.contains("member='Notify'"));
|
|
274
|
+
assert!(NOTIFY_RULE.contains("org.freedesktop.Notifications"));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
//! macOS notification probe: read the Notification Center database.
|
|
2
|
+
//!
|
|
3
|
+
//! Source only: this module is type-checked with `--features macos-check` but has
|
|
4
|
+
//! not been run on macOS. Notification Center stores a binary property list per
|
|
5
|
+
//! record; it is parsed in-process (no `plutil`/AppleScript fallback).
|
|
6
|
+
//!
|
|
7
|
+
//! Reading the database requires Full Disk Access for the DSH host. When access
|
|
8
|
+
//! is denied the probe returns a clear error instead of partial/fake data.
|
|
9
|
+
|
|
10
|
+
use std::path::PathBuf;
|
|
11
|
+
|
|
12
|
+
use plist::{Dictionary, Value};
|
|
13
|
+
use rusqlite::{types::ValueRef, Connection, OpenFlags};
|
|
14
|
+
|
|
15
|
+
use super::{truncate, NotificationItem, Snapshot, SnapshotResult};
|
|
16
|
+
use crate::cli::{CliError, CliResult};
|
|
17
|
+
use crate::util;
|
|
18
|
+
|
|
19
|
+
pub const SOURCE: &str = "macos-notification-center";
|
|
20
|
+
pub const LIVE_ONLY: bool = false;
|
|
21
|
+
|
|
22
|
+
pub fn watch(_limit: usize) -> CliResult {
|
|
23
|
+
Err(CliError::usage(
|
|
24
|
+
"notifications watch is available on Linux only".to_string(),
|
|
25
|
+
))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const APP_LABELS: &[(&str, &str)] = &[
|
|
29
|
+
("com.apple.Music", "音乐"),
|
|
30
|
+
("com.apple.iTunes", "音乐"),
|
|
31
|
+
("com.spotify.client", "Spotify"),
|
|
32
|
+
("com.tencent.xinWeChat", "微信"),
|
|
33
|
+
("com.tencent.qq", "QQ"),
|
|
34
|
+
("com.microsoft.teams2", "Microsoft Teams"),
|
|
35
|
+
("com.microsoft.Outlook", "Outlook"),
|
|
36
|
+
("com.google.Chrome", "Google Chrome"),
|
|
37
|
+
("com.apple.Safari", "Safari"),
|
|
38
|
+
("com.apple.mail", "邮件"),
|
|
39
|
+
("com.apple.reminders", "提醒事项"),
|
|
40
|
+
("com.apple.iCal", "日历"),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
fn database_path() -> Option<PathBuf> {
|
|
44
|
+
util::home_dir().map(|home| {
|
|
45
|
+
home.join("Library")
|
|
46
|
+
.join("Group Containers")
|
|
47
|
+
.join("group.com.apple.usernoted")
|
|
48
|
+
.join("db2")
|
|
49
|
+
.join("db")
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn quote(identifier: &str) -> String {
|
|
54
|
+
format!("\"{}\"", identifier.replace('"', "\"\""))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn value_string(value: ValueRef<'_>) -> Option<String> {
|
|
58
|
+
match value {
|
|
59
|
+
ValueRef::Text(bytes) => Some(String::from_utf8_lossy(bytes).into_owned()),
|
|
60
|
+
ValueRef::Integer(number) => Some(number.to_string()),
|
|
61
|
+
ValueRef::Real(number) => Some(number.to_string()),
|
|
62
|
+
_ => None,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn value_bytes(value: ValueRef<'_>) -> Option<Vec<u8>> {
|
|
67
|
+
match value {
|
|
68
|
+
ValueRef::Blob(bytes) => Some(bytes.to_vec()),
|
|
69
|
+
ValueRef::Text(bytes) => Some(bytes.to_vec()),
|
|
70
|
+
_ => None,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn tables(connection: &Connection) -> Vec<String> {
|
|
75
|
+
let Ok(mut statement) =
|
|
76
|
+
connection.prepare("select name from sqlite_master where type = 'table'")
|
|
77
|
+
else {
|
|
78
|
+
return Vec::new();
|
|
79
|
+
};
|
|
80
|
+
let Ok(rows) = statement.query_map([], |row| row.get::<_, String>(0)) else {
|
|
81
|
+
return Vec::new();
|
|
82
|
+
};
|
|
83
|
+
rows.flatten().collect()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn columns(connection: &Connection, table: &str) -> Vec<String> {
|
|
87
|
+
let sql = format!("pragma table_info({})", quote(table));
|
|
88
|
+
let Ok(mut statement) = connection.prepare(&sql) else {
|
|
89
|
+
return Vec::new();
|
|
90
|
+
};
|
|
91
|
+
let Ok(rows) = statement.query_map([], |row| row.get::<_, String>(1)) else {
|
|
92
|
+
return Vec::new();
|
|
93
|
+
};
|
|
94
|
+
rows.flatten().collect()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn pick_column<'a>(columns: &'a [String], predicate: impl Fn(&str) -> bool) -> Option<&'a str> {
|
|
98
|
+
columns
|
|
99
|
+
.iter()
|
|
100
|
+
.find(|name| predicate(&name.to_lowercase()))
|
|
101
|
+
.map(String::as_str)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn app_name(app_key: &str, app_table_names: &std::collections::HashMap<String, String>) -> String {
|
|
105
|
+
if let Some(name) = app_table_names.get(app_key) {
|
|
106
|
+
return name.clone();
|
|
107
|
+
}
|
|
108
|
+
if let Some((_, label)) = APP_LABELS.iter().find(|(key, _)| *key == app_key) {
|
|
109
|
+
return (*label).to_string();
|
|
110
|
+
}
|
|
111
|
+
app_key
|
|
112
|
+
.split('.')
|
|
113
|
+
.filter(|part| !part.is_empty())
|
|
114
|
+
.next_back()
|
|
115
|
+
.unwrap_or("macOS")
|
|
116
|
+
.to_string()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn pick_string(dictionary: &Dictionary, keys: &[&str]) -> String {
|
|
120
|
+
for key in keys {
|
|
121
|
+
let Some(value) = dictionary.get(key) else {
|
|
122
|
+
continue;
|
|
123
|
+
};
|
|
124
|
+
if let Some(text) = value.as_string() {
|
|
125
|
+
if !text.trim().is_empty() {
|
|
126
|
+
return text.trim().to_string();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if let Some(inner) = value.as_dictionary() {
|
|
130
|
+
for inner_key in ["string", "stringValue"] {
|
|
131
|
+
if let Some(text) = inner.get(inner_key).and_then(Value::as_string) {
|
|
132
|
+
if !text.trim().is_empty() {
|
|
133
|
+
return text.trim().to_string();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
String::new()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn parse_payload(payload: &[u8]) -> Option<(String, String, String)> {
|
|
143
|
+
let value = Value::from_reader(std::io::Cursor::new(payload)).ok()?;
|
|
144
|
+
let root = value.as_dictionary()?;
|
|
145
|
+
let request = root
|
|
146
|
+
.get("req")
|
|
147
|
+
.and_then(Value::as_dictionary)
|
|
148
|
+
.unwrap_or(root);
|
|
149
|
+
Some((
|
|
150
|
+
pick_string(request, &["titl", "title"]),
|
|
151
|
+
pick_string(request, &["subt", "subtitle"]),
|
|
152
|
+
pick_string(request, &["body", "mess"]),
|
|
153
|
+
))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn apple_time_to_ms(value: ValueRef<'_>) -> Option<i64> {
|
|
157
|
+
let number = match value {
|
|
158
|
+
ValueRef::Integer(number) => number as f64,
|
|
159
|
+
ValueRef::Real(number) => number,
|
|
160
|
+
ValueRef::Text(bytes) => String::from_utf8_lossy(bytes).trim().parse::<f64>().ok()?,
|
|
161
|
+
_ => return None,
|
|
162
|
+
};
|
|
163
|
+
if !number.is_finite() || number <= 0.0 {
|
|
164
|
+
return None;
|
|
165
|
+
}
|
|
166
|
+
let seconds = if number > 10_000_000_000.0 {
|
|
167
|
+
number / 1000.0
|
|
168
|
+
} else {
|
|
169
|
+
number
|
|
170
|
+
};
|
|
171
|
+
Some(((seconds + 978_307_200.0) * 1000.0).round() as i64)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
pub fn read(limit: usize) -> SnapshotResult {
|
|
175
|
+
let source = database_path().ok_or_else(|| "home directory not found".to_string())?;
|
|
176
|
+
if !source.is_file() {
|
|
177
|
+
return Err("Notification Center database not found".to_string());
|
|
178
|
+
}
|
|
179
|
+
let temp = util::TempDir::create("dsh-probe-nc")?;
|
|
180
|
+
let target = temp.path().join("db");
|
|
181
|
+
util::copy_sqlite_database(&source, &target)?;
|
|
182
|
+
let connection = Connection::open_with_flags(&target, OpenFlags::SQLITE_OPEN_READ_ONLY)
|
|
183
|
+
.map_err(|error| {
|
|
184
|
+
format!(
|
|
185
|
+
"Notification Center is not readable (grant Full Disk Access to the DSH host): {error}"
|
|
186
|
+
)
|
|
187
|
+
})?;
|
|
188
|
+
|
|
189
|
+
let table_names = tables(&connection);
|
|
190
|
+
let record_table = table_names
|
|
191
|
+
.iter()
|
|
192
|
+
.find(|name| name.eq_ignore_ascii_case("record"))
|
|
193
|
+
.or_else(|| {
|
|
194
|
+
table_names
|
|
195
|
+
.iter()
|
|
196
|
+
.find(|name| name.to_lowercase().contains("record"))
|
|
197
|
+
})
|
|
198
|
+
.ok_or_else(|| "Notification Center record table not found".to_string())?
|
|
199
|
+
.clone();
|
|
200
|
+
let record_columns = columns(&connection, &record_table);
|
|
201
|
+
let data_column = pick_column(&record_columns, |name| {
|
|
202
|
+
name == "data" || name == "plist" || name == "record"
|
|
203
|
+
})
|
|
204
|
+
.or_else(|| pick_column(&record_columns, |name| name.contains("data")))
|
|
205
|
+
.ok_or_else(|| "Notification Center payload column not found".to_string())?
|
|
206
|
+
.to_string();
|
|
207
|
+
let app_column = pick_column(&record_columns, |name| {
|
|
208
|
+
(name.contains("app") && (name.contains("id") || name.contains("bundle")))
|
|
209
|
+
|| name.contains("bundle")
|
|
210
|
+
})
|
|
211
|
+
.map(str::to_string);
|
|
212
|
+
let date_column = pick_column(&record_columns, |name| {
|
|
213
|
+
name.contains("deliver") || name.contains("date") || name.contains("time")
|
|
214
|
+
})
|
|
215
|
+
.map(str::to_string);
|
|
216
|
+
|
|
217
|
+
let mut select = format!("{} as payload", quote(&data_column));
|
|
218
|
+
select.push_str(&format!(
|
|
219
|
+
", {} as appId",
|
|
220
|
+
app_column
|
|
221
|
+
.as_deref()
|
|
222
|
+
.map(quote)
|
|
223
|
+
.unwrap_or_else(|| "''".to_string())
|
|
224
|
+
));
|
|
225
|
+
select.push_str(&format!(
|
|
226
|
+
", {} as deliveredAt",
|
|
227
|
+
date_column
|
|
228
|
+
.as_deref()
|
|
229
|
+
.map(quote)
|
|
230
|
+
.unwrap_or_else(|| "0".to_string())
|
|
231
|
+
));
|
|
232
|
+
let order = date_column
|
|
233
|
+
.as_deref()
|
|
234
|
+
.map(quote)
|
|
235
|
+
.unwrap_or_else(|| "rowid".to_string());
|
|
236
|
+
let sql = format!(
|
|
237
|
+
"select {select} from {} order by {order} desc limit ?1",
|
|
238
|
+
quote(&record_table)
|
|
239
|
+
);
|
|
240
|
+
let fetch = (limit.saturating_mul(3)).max(24) as i64;
|
|
241
|
+
let mut statement = connection
|
|
242
|
+
.prepare(&sql)
|
|
243
|
+
.map_err(|error| format!("notification query failed: {error}"))?;
|
|
244
|
+
let rows = statement
|
|
245
|
+
.query_map([fetch], |row| {
|
|
246
|
+
Ok((
|
|
247
|
+
value_bytes(row.get_ref(0)?).unwrap_or_default(),
|
|
248
|
+
value_string(row.get_ref(1)?).unwrap_or_default(),
|
|
249
|
+
apple_time_to_ms(row.get_ref(2)?),
|
|
250
|
+
))
|
|
251
|
+
})
|
|
252
|
+
.map_err(|error| format!("notification query failed: {error}"))?;
|
|
253
|
+
|
|
254
|
+
let mut app_table_names = std::collections::HashMap::new();
|
|
255
|
+
if let Some(app_table) = table_names
|
|
256
|
+
.iter()
|
|
257
|
+
.find(|name| name.eq_ignore_ascii_case("app"))
|
|
258
|
+
{
|
|
259
|
+
let app_columns = columns(&connection, app_table);
|
|
260
|
+
let id_column = pick_column(&app_columns, |name| {
|
|
261
|
+
name.contains("identifier") || name.contains("bundle")
|
|
262
|
+
});
|
|
263
|
+
let name_column = pick_column(&app_columns, |name| {
|
|
264
|
+
name.contains("name") || name.contains("display")
|
|
265
|
+
});
|
|
266
|
+
if let (Some(id_column), Some(name_column)) = (id_column, name_column) {
|
|
267
|
+
let sql = format!(
|
|
268
|
+
"select {}, {} from {}",
|
|
269
|
+
quote(id_column),
|
|
270
|
+
quote(name_column),
|
|
271
|
+
quote(app_table)
|
|
272
|
+
);
|
|
273
|
+
if let Ok(mut app_statement) = connection.prepare(&sql) {
|
|
274
|
+
if let Ok(app_rows) = app_statement.query_map([], |row| {
|
|
275
|
+
Ok((
|
|
276
|
+
value_string(row.get_ref(0)?).unwrap_or_default(),
|
|
277
|
+
value_string(row.get_ref(1)?).unwrap_or_default(),
|
|
278
|
+
))
|
|
279
|
+
}) {
|
|
280
|
+
for (key, name) in app_rows.flatten() {
|
|
281
|
+
if !key.is_empty() && !name.is_empty() {
|
|
282
|
+
app_table_names.insert(key, name);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let now = util::now_ms();
|
|
291
|
+
let mut notifications = Vec::new();
|
|
292
|
+
for (payload, app_id, delivered_at) in rows.flatten() {
|
|
293
|
+
if payload.is_empty() {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
let Some((title, subtitle, body)) = parse_payload(&payload) else {
|
|
297
|
+
continue;
|
|
298
|
+
};
|
|
299
|
+
if title.is_empty() && body.is_empty() && subtitle.is_empty() {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
let app_key = if app_id.trim().is_empty() {
|
|
303
|
+
"macOS".to_string()
|
|
304
|
+
} else {
|
|
305
|
+
app_id.trim().to_string()
|
|
306
|
+
};
|
|
307
|
+
let app_name = app_name(&app_key, &app_table_names);
|
|
308
|
+
let title = if title.is_empty() {
|
|
309
|
+
app_name.clone()
|
|
310
|
+
} else {
|
|
311
|
+
title
|
|
312
|
+
};
|
|
313
|
+
let body = [subtitle, body]
|
|
314
|
+
.into_iter()
|
|
315
|
+
.filter(|part| !part.is_empty())
|
|
316
|
+
.collect::<Vec<_>>()
|
|
317
|
+
.join(" ");
|
|
318
|
+
let received_at = delivered_at.unwrap_or(now);
|
|
319
|
+
notifications.push(NotificationItem {
|
|
320
|
+
id: format!("{received_at}-{}", notifications.len()),
|
|
321
|
+
app_key,
|
|
322
|
+
app_name,
|
|
323
|
+
title: truncate(&title, 160),
|
|
324
|
+
body: truncate(&body, 260),
|
|
325
|
+
avatar: None,
|
|
326
|
+
has_image: false,
|
|
327
|
+
received_at,
|
|
328
|
+
is_group: false,
|
|
329
|
+
});
|
|
330
|
+
if notifications.len() >= limit {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Ok(Snapshot {
|
|
336
|
+
source: SOURCE,
|
|
337
|
+
live_only: LIVE_ONLY,
|
|
338
|
+
notifications,
|
|
339
|
+
updated_at: now,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[cfg(test)]
|
|
344
|
+
mod tests {
|
|
345
|
+
use super::*;
|
|
346
|
+
|
|
347
|
+
#[test]
|
|
348
|
+
fn converts_apple_epoch_times() {
|
|
349
|
+
// 2024-01-01T00:00:00Z is 726_537_600 seconds after the 2001 epoch.
|
|
350
|
+
assert_eq!(
|
|
351
|
+
apple_time_to_ms(ValueRef::Real(726_537_600.0)),
|
|
352
|
+
Some(1_704_067_200_000)
|
|
353
|
+
);
|
|
354
|
+
// Millisecond values are detected and normalized.
|
|
355
|
+
assert_eq!(
|
|
356
|
+
apple_time_to_ms(ValueRef::Integer(1_704_067_200_000)),
|
|
357
|
+
Some(1_704_067_200_000)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn picks_nested_plist_strings() {
|
|
363
|
+
let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
364
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
365
|
+
<plist version="1.0"><dict><key>req</key><dict>
|
|
366
|
+
<key>titl</key><dict><key>string</key><string>Hello</string></dict>
|
|
367
|
+
<key>body</key><string>World</string>
|
|
368
|
+
</dict></dict></plist>"#;
|
|
369
|
+
let value = Value::from_reader(std::io::Cursor::new(plist.as_bytes())).unwrap();
|
|
370
|
+
let root = value.as_dictionary().unwrap();
|
|
371
|
+
let request = root.get("req").and_then(Value::as_dictionary).unwrap();
|
|
372
|
+
assert_eq!(pick_string(request, &["titl"]), "Hello");
|
|
373
|
+
assert_eq!(pick_string(request, &["body"]), "World");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn derives_app_names() {
|
|
378
|
+
let names = std::collections::HashMap::new();
|
|
379
|
+
assert_eq!(app_name("com.apple.Music", &names), "音乐");
|
|
380
|
+
assert_eq!(app_name("com.example.SomeApp", &names), "SomeApp");
|
|
381
|
+
}
|
|
382
|
+
}
|