pi-harness-whatsapp 0.1.1 → 0.1.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/controller.ts +4 -1
- package/src/log.ts +1 -1
- package/src/media.ts +1 -1
- package/src/paths.ts +21 -1
- package/src/settings.ts +1 -1
- package/src/voice/models.ts +2 -2
- package/src/wa/connection.ts +7 -2
- package/src/wa/lock.ts +1 -1
- package/src/wa/recents.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
- **Fixed file permissions:** the data folder and the WhatsApp login in it were readable by other users on the same machine. Every folder the extension creates is now private (only your user can open it), and existing installs are fixed the next time Pi starts.
|
|
6
|
+
|
|
3
7
|
## 0.1.1 — 2026-10-15
|
|
4
8
|
|
|
5
9
|
- **Voice is optional, and off by default.** `/whatsapp` → Voice → **Voice notes** has two ticks: understand voice notes (speech-to-text, 1.0 GB) and reply with voice notes (text-to-speech, 0.6 GB), plus "Delete all speech files". Leaving the menu asks before deleting the files of a part you unticked ("You set speech-to-text for removal. 828 MB will be freed."; No puts the tick back) and offers to download a part you ticked (now, or when first needed). Nothing is downloaded while voice is off.
|
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ Everything lives in `~/.pi/agent/pi-harness-whatsapp/`:
|
|
|
218
218
|
| `setup.json` | Only if the installer recorded it: programs installed just for the extension (see [Uninstall](#what-was-installed-for-it)) |
|
|
219
219
|
| `log/pi-harness-whatsapp.log` | Log: connection events and errors, no message text (unless debug logging is on). Rotated at 5 MB into `.log.1`. |
|
|
220
220
|
|
|
221
|
-
If you set `PI_HARNESS_WHATSAPP_DIR`, everything is in that folder instead. The extension writes nothing anywhere else, apart from Pi's own files (your sessions and its package list).
|
|
221
|
+
The folder is private: only your user can open it. If you set `PI_HARNESS_WHATSAPP_DIR`, everything is in that folder instead. The extension writes nothing anywhere else, apart from Pi's own files (your sessions and its package list).
|
|
222
222
|
|
|
223
223
|
## Uninstall
|
|
224
224
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-whatsapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Talk to Pi from WhatsApp: images, files and optional voice notes in and out, automatic replies, group chats, poll menus to control Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/controller.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { cleanInbox, Media } from "./media.ts";
|
|
|
20
20
|
import type { SpeechInfo } from "./panel-model.ts";
|
|
21
21
|
import { getLanguage } from "./languages.ts";
|
|
22
22
|
import { type ConnectionCommand, type PanelHost, showPanel } from "./panel.ts";
|
|
23
|
-
import { dataDir, paths, removeDataDir } from "./paths.ts";
|
|
23
|
+
import { dataDir, makePrivate, paths, removeDataDir } from "./paths.ts";
|
|
24
24
|
import { isRemoved, markRemoved } from "./removed.ts";
|
|
25
25
|
import { detectService } from "./commands/power.ts";
|
|
26
26
|
import {
|
|
@@ -130,6 +130,9 @@ export class Controller implements PanelHost, CommandEnv {
|
|
|
130
130
|
ctx.ui.setStatus(STATUS_KEY, REMOVED_STATUS);
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
133
|
+
// Installs from before 0.1.2 left the data folder (and the login in it) readable by other users.
|
|
134
|
+
makePrivate(dataDir());
|
|
135
|
+
makePrivate(paths.auth(), true);
|
|
133
136
|
let settings = this.settings.load();
|
|
134
137
|
this.recents.load();
|
|
135
138
|
cleanInbox(paths.inbox());
|
package/src/log.ts
CHANGED
|
@@ -39,7 +39,7 @@ function serialize(value: unknown): string {
|
|
|
39
39
|
function write(level: Level, scope: string, parts: unknown[]): void {
|
|
40
40
|
if (LEVELS[level] < LEVELS[minLevel] || isRemoved()) return;
|
|
41
41
|
try {
|
|
42
|
-
mkdirSync(paths.logs(), { recursive: true });
|
|
42
|
+
mkdirSync(paths.logs(), { recursive: true, mode: 0o700 });
|
|
43
43
|
const target = file();
|
|
44
44
|
rotateIfNeeded(target);
|
|
45
45
|
const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(5)} [${scope}] ${parts.map(serialize).join(" ")}\n`;
|
package/src/media.ts
CHANGED
|
@@ -171,7 +171,7 @@ export class Media implements MediaHandler {
|
|
|
171
171
|
try {
|
|
172
172
|
const data = await this.deps.connection.download(message.raw);
|
|
173
173
|
if (data.length > MAX_FILE_BYTES) return { content: `${header}${quoted} (sent ${what} too large to save)`, voice: false };
|
|
174
|
-
mkdirSync(this.deps.inboxDir, { recursive: true });
|
|
174
|
+
mkdirSync(this.deps.inboxDir, { recursive: true, mode: 0o700 });
|
|
175
175
|
const name = safeFileName(message.fileName, EXT_BY_MIME[message.mimeType ?? ""] ?? (message.kind === "video" ? ".mp4" : ".bin"));
|
|
176
176
|
const path = join(this.deps.inboxDir, name);
|
|
177
177
|
writeFileSync(path, data, { mode: 0o600 });
|
package/src/paths.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -12,6 +12,26 @@ export function dataDir(): string {
|
|
|
12
12
|
return join(agentDir, "pi-harness-whatsapp");
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Folders the extension creates: only this user may open them (the data folder holds the WhatsApp login). */
|
|
16
|
+
export const PRIVATE_DIR_MODE = 0o700;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Makes a folder private (owner only), and with `files` also the files directly
|
|
20
|
+
* in it. Fixes installs from before 0.1.2, whose login was readable by other users.
|
|
21
|
+
*/
|
|
22
|
+
export function makePrivate(dir: string, files = false): void {
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(dir)) return;
|
|
25
|
+
chmodSync(dir, PRIVATE_DIR_MODE);
|
|
26
|
+
if (!files) return;
|
|
27
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
if (entry.isFile()) chmodSync(join(dir, entry.name), 0o600);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Not every file system has Unix permissions (e.g. Windows); nothing to fix there.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
export const paths = {
|
|
16
36
|
config: () => join(dataDir(), "config.json"),
|
|
17
37
|
auth: () => join(dataDir(), "auth"),
|
package/src/settings.ts
CHANGED
|
@@ -271,7 +271,7 @@ export class SettingsStore {
|
|
|
271
271
|
|
|
272
272
|
private save(): void {
|
|
273
273
|
if (isRemoved()) return;
|
|
274
|
-
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
274
|
+
mkdirSync(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
275
275
|
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
276
276
|
writeFileSync(temporary, `${JSON.stringify(this.settings, null, 2)}\n`, { mode: 0o600 });
|
|
277
277
|
renameSync(temporary, this.filePath);
|
package/src/voice/models.ts
CHANGED
|
@@ -202,7 +202,7 @@ export class ModelStore {
|
|
|
202
202
|
|
|
203
203
|
private async install(spec: ModelSpec): Promise<string> {
|
|
204
204
|
if (!spec.hf) throw new Error(`${spec.name} can't be downloaded`);
|
|
205
|
-
mkdirSync(this.root, { recursive: true });
|
|
205
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
206
206
|
const staging = this.staging(spec);
|
|
207
207
|
log.info(`downloading ${spec.hf.repo}@${spec.hf.revision.slice(0, 8)}`);
|
|
208
208
|
this.onProgress?.(spec, 0);
|
|
@@ -244,7 +244,7 @@ export class ModelStore {
|
|
|
244
244
|
if (!runtimeSupported(os, cpu)) {
|
|
245
245
|
throw new Error(`OpenVINO has no build for ${os}/${cpu}, so voice notes can't work on this machine`);
|
|
246
246
|
}
|
|
247
|
-
mkdirSync(this.root, { recursive: true });
|
|
247
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
248
248
|
const staging = join(this.root, `.${spec.name}.downloading`);
|
|
249
249
|
this.onProgress?.(spec, 0);
|
|
250
250
|
try {
|
package/src/wa/connection.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* talks to Baileys' socket directly, so a Baileys upgrade stays contained.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { makePrivate } from "../paths.ts";
|
|
9
10
|
import makeWASocket, {
|
|
10
11
|
type AnyMessageContent,
|
|
11
12
|
Browsers,
|
|
@@ -298,6 +299,10 @@ export class WhatsAppConnection {
|
|
|
298
299
|
const generation = ++this.generation;
|
|
299
300
|
this.setState({ kind: "connecting", attempt: this.attempt });
|
|
300
301
|
try {
|
|
302
|
+
// The login lets anyone use the account: only this user may open its folder.
|
|
303
|
+
mkdirSync(this.authDir, { recursive: true, mode: 0o700 });
|
|
304
|
+
makePrivate(dirname(this.authDir));
|
|
305
|
+
makePrivate(this.authDir, true);
|
|
301
306
|
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
|
|
302
307
|
if (generation !== this.generation || !this.wantRunning) return;
|
|
303
308
|
this.version ??= await this.fetchVersion();
|
package/src/wa/lock.ts
CHANGED
|
@@ -74,7 +74,7 @@ export class ConnectionLock {
|
|
|
74
74
|
|
|
75
75
|
/** Takes the lock unless another live process on this machine holds it. */
|
|
76
76
|
acquire(): LockResult {
|
|
77
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
77
|
+
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
78
78
|
const me: LockHolder = { pid: this.pid, host: hostname(), since: Date.now() };
|
|
79
79
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
80
80
|
try {
|
package/src/wa/recents.ts
CHANGED
|
@@ -99,7 +99,7 @@ export class RecentsStore {
|
|
|
99
99
|
if (!this.dirty || isRemoved()) return;
|
|
100
100
|
this.dirty = false;
|
|
101
101
|
try {
|
|
102
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
102
|
+
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
103
103
|
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
104
104
|
const data = { chats: this.list().slice(0, MAX_CHATS), lids: [...this.lidToPhone] };
|
|
105
105
|
writeFileSync(temporary, JSON.stringify(data), { mode: 0o600 });
|