pinokiod 3.130.0 → 3.131.0
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/kernel/api/push/index.js +9 -5
- package/kernel/util.js +195 -14
- package/package.json +2 -2
- package/server/index.js +73 -49
package/kernel/api/push/index.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/*
|
|
2
2
|
{
|
|
3
3
|
method: "push",
|
|
4
|
-
|
|
4
|
+
params: {
|
|
5
5
|
title: <string>,
|
|
6
6
|
subtitle: <string>,
|
|
7
7
|
message: <string>,
|
|
8
8
|
image: <image path>,
|
|
9
|
-
sound: true|false,
|
|
10
|
-
}
|
|
9
|
+
sound: true|false|string,
|
|
11
10
|
}
|
|
11
|
+
}
|
|
12
12
|
(*/
|
|
13
13
|
const util = require('../../util')
|
|
14
14
|
const path = require('path');
|
|
15
15
|
module.exports = async (req, ondata, kernel) => {
|
|
16
|
-
|
|
17
|
-
if (params.image) {
|
|
16
|
+
const params = { ...(req.params || {}) }
|
|
17
|
+
if (typeof params.image === 'string') {
|
|
18
18
|
params.image = path.resolve(req.cwd, params.image)
|
|
19
19
|
}
|
|
20
|
+
if (typeof params.sound === 'string') {
|
|
21
|
+
const trimmed = params.sound.trim()
|
|
22
|
+
params.sound = trimmed || undefined
|
|
23
|
+
}
|
|
20
24
|
util.push(params)
|
|
21
25
|
}
|
package/kernel/util.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
-
const { spawn } = require('child_process')
|
|
2
|
+
const { spawn, spawnSync } = require('child_process')
|
|
3
3
|
const Clipboard = require('copy-paste/promises');
|
|
4
4
|
const http = require('http');
|
|
5
|
-
const notifier = require('
|
|
5
|
+
const notifier = require('toasted-notifier');
|
|
6
6
|
const os = require('os')
|
|
7
7
|
const net = require('node:net')
|
|
8
8
|
const path = require('path')
|
|
@@ -12,6 +12,11 @@ const retry = require('async-retry');
|
|
|
12
12
|
const child_process = require('node:child_process');
|
|
13
13
|
const {auto: normalizeEOL} = require("eol");
|
|
14
14
|
const {EOL} = require("os");
|
|
15
|
+
const axios = require('axios');
|
|
16
|
+
const { pipeline } = require('stream/promises');
|
|
17
|
+
const { pathToFileURL } = require('url');
|
|
18
|
+
const { randomUUID } = require('crypto');
|
|
19
|
+
const fsp = fs.promises;
|
|
15
20
|
const breakPattern = /\n/g;
|
|
16
21
|
const breakReplacement = "\\n";
|
|
17
22
|
const groupPattern = /\$/g;
|
|
@@ -24,7 +29,26 @@ const {
|
|
|
24
29
|
} = require('glob')
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
const platform = os.platform()
|
|
33
|
+
function ensureNotifierBinaries() {
|
|
34
|
+
if (platform !== 'darwin') {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const notifierModulePath = require.resolve('toasted-notifier/notifiers/notificationcenter')
|
|
39
|
+
const binaryPath = path.resolve(path.dirname(notifierModulePath), '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier')
|
|
40
|
+
if (!fs.existsSync(binaryPath)) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const mode = fs.statSync(binaryPath).mode & 0o777
|
|
44
|
+
if ((mode & 0o111) === 0) {
|
|
45
|
+
fs.chmodSync(binaryPath, mode | 0o755)
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn('Warning: unable to update terminal-notifier permissions:', err.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
ensureNotifierBinaries()
|
|
28
52
|
// asar handling for go-get-folder-size
|
|
29
53
|
let g
|
|
30
54
|
if( __dirname.includes(".asar") ) {
|
|
@@ -530,6 +554,147 @@ function fill_object(obj, pattern, list, cache) {
|
|
|
530
554
|
let res = recurse(obj);
|
|
531
555
|
return { result: res, replaced_map }
|
|
532
556
|
}
|
|
557
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0
|
|
558
|
+
const HTTP_URL_REGEX = /^https?:\/\//i
|
|
559
|
+
const linuxSoundCandidates = [
|
|
560
|
+
{ cmd: 'paplay', args: (filePath) => [filePath] },
|
|
561
|
+
{ cmd: 'aplay', args: (filePath) => [filePath] },
|
|
562
|
+
{ cmd: 'ffplay', args: (filePath) => ['-autoexit', '-nodisp', filePath] },
|
|
563
|
+
{ cmd: 'mpg123', args: (filePath) => [filePath] },
|
|
564
|
+
{ cmd: 'mplayer', args: (filePath) => [filePath] },
|
|
565
|
+
{ cmd: 'cvlc', args: (filePath) => ['--play-and-exit', filePath] },
|
|
566
|
+
]
|
|
567
|
+
let cachedLinuxSoundPlayer
|
|
568
|
+
const DEFAULT_CHIME_PATH = path.resolve(__dirname, '../server/public/chime.mp3')
|
|
569
|
+
function pickLinuxSoundPlayer() {
|
|
570
|
+
if (cachedLinuxSoundPlayer !== undefined) {
|
|
571
|
+
return cachedLinuxSoundPlayer
|
|
572
|
+
}
|
|
573
|
+
for (const candidate of linuxSoundCandidates) {
|
|
574
|
+
try {
|
|
575
|
+
const lookup = spawnSync('which', [candidate.cmd], { stdio: 'ignore' })
|
|
576
|
+
if (lookup.status === 0) {
|
|
577
|
+
cachedLinuxSoundPlayer = candidate
|
|
578
|
+
return cachedLinuxSoundPlayer
|
|
579
|
+
}
|
|
580
|
+
} catch (_) {
|
|
581
|
+
// ignore lookup errors and try next candidate
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
cachedLinuxSoundPlayer = null
|
|
585
|
+
return cachedLinuxSoundPlayer
|
|
586
|
+
}
|
|
587
|
+
function extractUrlExtension(rawUrl) {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = new URL(rawUrl)
|
|
590
|
+
const ext = path.extname(parsed.pathname)
|
|
591
|
+
if (ext && ext.length <= 6) {
|
|
592
|
+
return ext
|
|
593
|
+
}
|
|
594
|
+
} catch (_) {
|
|
595
|
+
// ignore URL parse errors
|
|
596
|
+
}
|
|
597
|
+
return ''
|
|
598
|
+
}
|
|
599
|
+
async function downloadSoundToTemp(url) {
|
|
600
|
+
const tempPath = path.join(os.tmpdir(), `pinokio-notify-${randomUUID()}${extractUrlExtension(url)}`)
|
|
601
|
+
const response = await axios.get(url, {
|
|
602
|
+
responseType: 'stream',
|
|
603
|
+
timeout: 15000,
|
|
604
|
+
})
|
|
605
|
+
await pipeline(response.data, fs.createWriteStream(tempPath))
|
|
606
|
+
return {
|
|
607
|
+
filePath: tempPath,
|
|
608
|
+
cleanup: async () => {
|
|
609
|
+
try {
|
|
610
|
+
await fsp.unlink(tempPath)
|
|
611
|
+
} catch (_) {
|
|
612
|
+
// best-effort cleanup
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function playSoundFile(filePath) {
|
|
618
|
+
if (!filePath) {
|
|
619
|
+
return null
|
|
620
|
+
}
|
|
621
|
+
if (platform === 'darwin') {
|
|
622
|
+
return spawn('afplay', [filePath], { stdio: 'ignore' })
|
|
623
|
+
}
|
|
624
|
+
if (platform === 'win32') {
|
|
625
|
+
const fileUrl = pathToFileURL(filePath).href.replace(/'/g, "''")
|
|
626
|
+
const script = [
|
|
627
|
+
"$ErrorActionPreference = 'Stop'",
|
|
628
|
+
"Add-Type -AssemblyName presentationCore",
|
|
629
|
+
"$player = New-Object system.windows.media.mediaplayer",
|
|
630
|
+
`$player.Open([Uri]'${fileUrl}')`,
|
|
631
|
+
"$player.Play()",
|
|
632
|
+
"while ($player.NaturalDuration.HasTimeSpan -eq $false) { Start-Sleep -Milliseconds 100 }",
|
|
633
|
+
"Start-Sleep -Seconds $player.NaturalDuration.TimeSpan.TotalSeconds",
|
|
634
|
+
].join('; ')
|
|
635
|
+
return spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], {
|
|
636
|
+
stdio: 'ignore'
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
const candidate = pickLinuxSoundPlayer()
|
|
640
|
+
if (!candidate) {
|
|
641
|
+
throw new Error('No supported audio player found for Linux (expected one of paplay, aplay, ffplay, mpg123, mplayer, cvlc).')
|
|
642
|
+
}
|
|
643
|
+
return spawn(candidate.cmd, candidate.args(filePath), { stdio: 'ignore' })
|
|
644
|
+
}
|
|
645
|
+
function scheduleSoundPlayback(target) {
|
|
646
|
+
if (!target) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
;(async () => {
|
|
650
|
+
let cleanup = null
|
|
651
|
+
try {
|
|
652
|
+
let filePath
|
|
653
|
+
if (target.kind === 'file') {
|
|
654
|
+
filePath = target.value
|
|
655
|
+
} else if (target.kind === 'url') {
|
|
656
|
+
if (!HTTP_URL_REGEX.test(target.value)) {
|
|
657
|
+
throw new Error(`Unsupported sound URL: ${target.value}`)
|
|
658
|
+
}
|
|
659
|
+
const downloaded = await downloadSoundToTemp(target.value)
|
|
660
|
+
filePath = downloaded.filePath
|
|
661
|
+
cleanup = downloaded.cleanup
|
|
662
|
+
} else {
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const child = playSoundFile(filePath)
|
|
667
|
+
if (!child) {
|
|
668
|
+
if (cleanup) {
|
|
669
|
+
cleanup().catch(() => {})
|
|
670
|
+
}
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const tidy = () => {
|
|
675
|
+
if (!cleanup) {
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
cleanup().catch((err) => {
|
|
679
|
+
console.error('Failed to clean up temporary sound file:', err)
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
child.once('close', tidy)
|
|
684
|
+
child.once('error', tidy)
|
|
685
|
+
child.on('error', (err) => {
|
|
686
|
+
console.error('Failed to play notification sound:', err)
|
|
687
|
+
})
|
|
688
|
+
} catch (err) {
|
|
689
|
+
if (cleanup) {
|
|
690
|
+
cleanup().catch(() => {})
|
|
691
|
+
}
|
|
692
|
+
console.error('Failed to play notification sound:', err)
|
|
693
|
+
}
|
|
694
|
+
})().catch((err) => {
|
|
695
|
+
console.error('Failed to play notification sound:', err)
|
|
696
|
+
})
|
|
697
|
+
}
|
|
533
698
|
function push(params) {
|
|
534
699
|
/*
|
|
535
700
|
params :- {
|
|
@@ -537,23 +702,39 @@ function push(params) {
|
|
|
537
702
|
subtitle: <string>,
|
|
538
703
|
message: <string>,
|
|
539
704
|
image: <image path>,
|
|
540
|
-
sound: true|false,
|
|
705
|
+
sound: true|false|string, // true uses default chime
|
|
541
706
|
}
|
|
542
707
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
708
|
+
const notifyParams = { ...(params || {}) }
|
|
709
|
+
const customSoundTarget = (() => {
|
|
710
|
+
if (notifyParams.sound === true) {
|
|
711
|
+
return { kind: 'file', value: DEFAULT_CHIME_PATH }
|
|
712
|
+
}
|
|
713
|
+
if (isNonEmptyString(notifyParams.sound)) {
|
|
714
|
+
const trimmed = notifyParams.sound.trim()
|
|
715
|
+
if (!trimmed) {
|
|
716
|
+
return null
|
|
717
|
+
}
|
|
718
|
+
return { kind: 'url', value: trimmed }
|
|
719
|
+
}
|
|
720
|
+
return null
|
|
721
|
+
})()
|
|
722
|
+
|
|
723
|
+
if (notifyParams.image && !notifyParams.contentImage) {
|
|
724
|
+
notifyParams.contentImage = notifyParams.image
|
|
545
725
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
params.contentImage = params.image
|
|
726
|
+
if (!notifyParams.title) {
|
|
727
|
+
notifyParams.title = "Pinokio"
|
|
549
728
|
}
|
|
550
|
-
if (!
|
|
551
|
-
|
|
729
|
+
if (!notifyParams.contentImage) {
|
|
730
|
+
notifyParams.contentImage = path.resolve(__dirname, "../server/public/pinokio-black.png")
|
|
552
731
|
}
|
|
553
|
-
if (
|
|
554
|
-
|
|
732
|
+
if (customSoundTarget) {
|
|
733
|
+
notifyParams.sound = false
|
|
734
|
+
scheduleSoundPlayback(customSoundTarget)
|
|
555
735
|
}
|
|
556
|
-
|
|
736
|
+
console.log("notifyParams", notifyParams)
|
|
737
|
+
notifier.notify(notifyParams)
|
|
557
738
|
}
|
|
558
739
|
function p2u(localPath) {
|
|
559
740
|
/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pinokiod",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.131.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
"multer": "^1.4.5-lts.1",
|
|
61
61
|
"node-downloader-helper": "^2.1.9",
|
|
62
62
|
"node-gradio-client": "^0.14.6",
|
|
63
|
-
"node-notifier": "^10.0.1",
|
|
64
63
|
"normalize-path": "^3.0.0",
|
|
65
64
|
"p-limit": "^3.1.0",
|
|
66
65
|
"parseurl": "^1.3.3",
|
|
@@ -77,6 +76,7 @@
|
|
|
77
76
|
"sudo-prompt-programfiles-x86": "^9.2.10",
|
|
78
77
|
"symlink-dir": "^5.2.1",
|
|
79
78
|
"systeminformation": "^5.18.4",
|
|
79
|
+
"toasted-notifier": "^10.1.0",
|
|
80
80
|
"twitter-api-v2": "^1.23.2",
|
|
81
81
|
"uuid": "^9.0.0",
|
|
82
82
|
"wait-on": "^7.2.0",
|
package/server/index.js
CHANGED
|
@@ -4521,64 +4521,88 @@ class Server {
|
|
|
4521
4521
|
console.log("Push", req.body)
|
|
4522
4522
|
try {
|
|
4523
4523
|
const payload = { ...(req.body || {}) }
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
4535
|
-
try {
|
|
4536
|
-
const parsed = new URL(trimmed)
|
|
4537
|
-
candidate = parsed.pathname
|
|
4538
|
-
} catch (_) {
|
|
4539
|
-
return null
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
if (!candidate.startsWith('/asset/')) {
|
|
4543
|
-
return null
|
|
4544
|
-
}
|
|
4545
|
-
const pathPart = candidate.split('?')[0].split('#')[0]
|
|
4546
|
-
const rel = pathPart.replace(/^\/asset\/+/, '')
|
|
4547
|
-
if (!rel) {
|
|
4548
|
-
return null
|
|
4549
|
-
}
|
|
4550
|
-
const parts = rel.split('/').filter(Boolean)
|
|
4551
|
-
if (!parts.length || parts.some((part) => part === '..')) {
|
|
4552
|
-
return null
|
|
4553
|
-
}
|
|
4524
|
+
const resolveAssetPath = (raw) => {
|
|
4525
|
+
if (typeof raw !== 'string') {
|
|
4526
|
+
return null
|
|
4527
|
+
}
|
|
4528
|
+
const trimmed = raw.trim()
|
|
4529
|
+
if (!trimmed) {
|
|
4530
|
+
return null
|
|
4531
|
+
}
|
|
4532
|
+
let candidate = trimmed
|
|
4533
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
4554
4534
|
try {
|
|
4555
|
-
|
|
4535
|
+
const parsed = new URL(trimmed)
|
|
4536
|
+
candidate = parsed.pathname
|
|
4556
4537
|
} catch (_) {
|
|
4557
4538
|
return null
|
|
4558
4539
|
}
|
|
4559
4540
|
}
|
|
4560
|
-
|
|
4541
|
+
if (!candidate.startsWith('/asset/')) {
|
|
4542
|
+
return null
|
|
4543
|
+
}
|
|
4544
|
+
const pathPart = candidate.split('?')[0].split('#')[0]
|
|
4545
|
+
const rel = pathPart.replace(/^\/asset\/+/, '')
|
|
4546
|
+
if (!rel) {
|
|
4547
|
+
return null
|
|
4548
|
+
}
|
|
4549
|
+
const parts = rel.split('/').filter(Boolean)
|
|
4550
|
+
if (!parts.length || parts.some((part) => part === '..')) {
|
|
4551
|
+
return null
|
|
4552
|
+
}
|
|
4553
|
+
try {
|
|
4554
|
+
return this.kernel.path(...parts)
|
|
4555
|
+
} catch (_) {
|
|
4556
|
+
return null
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
const resolvePublicAsset = async (raw) => {
|
|
4560
|
+
if (typeof raw !== 'string') {
|
|
4561
|
+
return null
|
|
4562
|
+
}
|
|
4563
|
+
const trimmed = raw.trim()
|
|
4564
|
+
if (!trimmed || !trimmed.startsWith('/')) {
|
|
4565
|
+
return null
|
|
4566
|
+
}
|
|
4567
|
+
const relative = trimmed.replace(/^\/+/, '')
|
|
4568
|
+
if (!relative) {
|
|
4569
|
+
return null
|
|
4570
|
+
}
|
|
4571
|
+
const publicRoot = path.resolve(__dirname, 'public')
|
|
4572
|
+
const candidate = path.resolve(publicRoot, relative)
|
|
4573
|
+
if (!candidate.startsWith(publicRoot)) {
|
|
4574
|
+
return null
|
|
4575
|
+
}
|
|
4576
|
+
try {
|
|
4577
|
+
await fs.promises.access(candidate, fs.constants.R_OK)
|
|
4578
|
+
return candidate
|
|
4579
|
+
} catch (_) {
|
|
4580
|
+
return null
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
const normaliseNotificationAsset = async (raw) => {
|
|
4584
|
+
const asset = resolveAssetPath(raw)
|
|
4585
|
+
if (asset) {
|
|
4586
|
+
return asset
|
|
4587
|
+
}
|
|
4588
|
+
const fallback = await resolvePublicAsset(raw)
|
|
4589
|
+
if (fallback) {
|
|
4590
|
+
return fallback
|
|
4591
|
+
}
|
|
4592
|
+
return null
|
|
4593
|
+
}
|
|
4594
|
+
if (typeof payload.image === 'string' && payload.image.trim()) {
|
|
4595
|
+
const resolvedImage = await normaliseNotificationAsset(payload.image)
|
|
4561
4596
|
if (resolvedImage) {
|
|
4562
4597
|
payload.image = resolvedImage
|
|
4563
|
-
} else {
|
|
4564
|
-
const normalised = payload.image.trim()
|
|
4565
|
-
if (normalised.startsWith('/')) {
|
|
4566
|
-
const relative = normalised.replace(/^\/+/, '')
|
|
4567
|
-
if (relative) {
|
|
4568
|
-
const publicRoot = path.resolve(__dirname, 'public')
|
|
4569
|
-
const candidate = path.resolve(publicRoot, relative)
|
|
4570
|
-
if (candidate.startsWith(publicRoot)) {
|
|
4571
|
-
try {
|
|
4572
|
-
await fs.promises.access(candidate, fs.constants.R_OK)
|
|
4573
|
-
payload.image = candidate
|
|
4574
|
-
} catch (_) {
|
|
4575
|
-
// ignore missing fallback asset
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
4598
|
}
|
|
4581
4599
|
}
|
|
4600
|
+
if (typeof payload.sound === 'string') {
|
|
4601
|
+
const trimmedSound = payload.sound.trim()
|
|
4602
|
+
payload.sound = trimmedSound || undefined
|
|
4603
|
+
}
|
|
4604
|
+
delete payload.soundUrl
|
|
4605
|
+
delete payload.soundPath
|
|
4582
4606
|
Util.push(payload)
|
|
4583
4607
|
res.json({ success: true })
|
|
4584
4608
|
} catch (e) {
|