@songsid/agend 2.3.0-beta.21 → 2.3.0-beta.22
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/dist/agent-endpoint.d.ts +8 -0
- package/dist/agent-endpoint.js +11 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +2 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/grok.js +56 -3
- package/dist/backend/grok.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/config-validator.js +8 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +14 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +14 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +28 -2
- package/dist/fleet-manager.js +102 -15
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +2 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +19 -6
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +25 -10
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/provider-secret-registry.d.ts +5 -3
- package/dist/provider-secret-registry.js +8 -2
- package/dist/provider-secret-registry.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +6 -0
- package/dist/public-tunnel-config.js +24 -0
- package/dist/public-tunnel-config.js.map +1 -0
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +3 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +13 -2
- package/dist/public-web-link.js +35 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/secret-store.d.ts +2 -0
- package/dist/secret-store.js +21 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/settings-api.d.ts +1 -0
- package/dist/settings-api.js +13 -9
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +4 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +6 -15
- package/dist/tunnel/cloudflared.js +20 -9
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/ui/app.html +2 -0
- package/dist/ui/chat-render.js +39 -0
- package/dist/ui/chat-store.js +8 -0
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/fleet-canvas.js +14 -6
- package/dist/ui/fleet-route-scene.js +86 -21
- package/dist/ui/fleet-traffic.js +57 -10
- package/dist/ui/icons/icon-192.png +0 -0
- package/dist/ui/icons/icon-512.png +0 -0
- package/dist/ui/panel-settings.js +57 -4
- package/dist/ui/settings-appearance.js +126 -1
- package/dist/ui/settings-learn.js +1 -0
- package/dist/ui/settings-strings.js +78 -0
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +47 -21
- package/dist/ui/signin.html +2 -0
- package/dist/web-api.d.ts +6 -2
- package/dist/web-api.js +114 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon-worker.d.ts +1 -0
- package/dist/web-app-icon-worker.js +15 -0
- package/dist/web-app-icon-worker.js.map +1 -0
- package/dist/web-app-icon.d.ts +107 -0
- package/dist/web-app-icon.js +256 -0
- package/dist/web-app-icon.js.map +1 -0
- package/dist/web-chat-history.d.ts +23 -0
- package/dist/web-chat-history.js +32 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +7 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-icons.d.ts +33 -1
- package/dist/web-icons.js +83 -11
- package/dist/web-icons.js.map +1 -1
- package/dist/web-png.d.ts +29 -0
- package/dist/web-png.js +321 -0
- package/dist/web-png.js.map +1 -0
- package/dist/web-reactions.d.ts +34 -0
- package/dist/web-reactions.js +47 -0
- package/dist/web-reactions.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2308 review: the app icon's conversion (decode, five resizes, PNG/ICO encoding) runs here, off the fleet's event
|
|
3
|
+
* loop — a 2048 px 16-bit source takes about a second. One source in (workerData), the made files or a reason out.
|
|
4
|
+
*/
|
|
5
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
6
|
+
import { appIconSet } from "./web-app-icon.js";
|
|
7
|
+
import { PngError } from "./web-png.js";
|
|
8
|
+
try {
|
|
9
|
+
const files = appIconSet(new Uint8Array(workerData.source));
|
|
10
|
+
parentPort.postMessage({ ok: true, files });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
parentPort.postMessage({ ok: false, detail: err instanceof PngError ? err.message : "the image could not be read" });
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=web-app-icon-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-app-icon-worker.js","sourceRoot":"","sources":["../src/web-app-icon-worker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,IAAI,CAAC;IACH,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,UAAU,CAAE,UAAsC,CAAC,MAAM,CAAC,CAAC,CAAC;IACzF,UAAW,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC/C,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,UAAW,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,EAAE,CAAC,CAAC;AACxH,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** What an upload may weigh (the route stops reading past it). */
|
|
2
|
+
export declare const APP_ICON_MAX_BYTES: number;
|
|
3
|
+
/** The source's side: at least the manifest's largest icon, at most what decodes in a few MB of memory. */
|
|
4
|
+
export declare const APP_ICON_MIN_SIDE = 512;
|
|
5
|
+
export declare const APP_ICON_MAX_SIDE = 2048;
|
|
6
|
+
/** The files made from the source, by the name each is served as. */
|
|
7
|
+
export declare const APP_ICON_FILES: readonly ["apple-touch-icon.png", "favicon.ico", "icon-192.png", "icon-512.png"];
|
|
8
|
+
export type AppIconFile = (typeof APP_ICON_FILES)[number];
|
|
9
|
+
export interface AppIconState {
|
|
10
|
+
version: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
by: string;
|
|
13
|
+
}
|
|
14
|
+
export type AppIconResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
state: AppIconState;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
status: 400 | 409 | 413 | 415 | 500 | 503;
|
|
20
|
+
error: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
};
|
|
23
|
+
/** A source checked and made into its files (prepare), not yet in force (commit). */
|
|
24
|
+
export interface AppIconPrepared {
|
|
25
|
+
version: string;
|
|
26
|
+
source: Uint8Array;
|
|
27
|
+
files: Record<AppIconFile, Buffer>;
|
|
28
|
+
epoch: number;
|
|
29
|
+
}
|
|
30
|
+
export type AppIconPrepareResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
prepared: AppIconPrepared;
|
|
33
|
+
} | Extract<AppIconResult, {
|
|
34
|
+
ok: false;
|
|
35
|
+
}>;
|
|
36
|
+
/** The conversion's bounds off the event loop: its time, and its worker's heap. */
|
|
37
|
+
export declare const APP_ICON_CONVERT_MS = 30000;
|
|
38
|
+
/** Make the files from a source somewhere: a worker thread in the fleet (the default), in-process where a test says so. */
|
|
39
|
+
export type AppIconConvert = (bytes: Uint8Array) => Promise<{
|
|
40
|
+
ok: true;
|
|
41
|
+
files: Record<AppIconFile, Buffer>;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
detail: string;
|
|
45
|
+
}>;
|
|
46
|
+
/** What the conversion needs of a worker thread (a test gives one that ends when it says). */
|
|
47
|
+
export interface IconWorker {
|
|
48
|
+
on(event: "message" | "error" | "exit", listener: (...args: any[]) => void): unknown;
|
|
49
|
+
terminate(): Promise<number>;
|
|
50
|
+
}
|
|
51
|
+
/** The fleet's worker: its own heap; TypeScript sources through tsx, the built .js otherwise. */
|
|
52
|
+
export declare function spawnIconWorker(source: ArrayBuffer): IconWorker;
|
|
53
|
+
/**
|
|
54
|
+
* The conversion in a one-off worker thread: its own heap, a deadline (terminated past it), never the fleet's loop.
|
|
55
|
+
* It settles only once the worker has exited (#2319 review): a timed-out or finished conversion whose thread is still
|
|
56
|
+
* winding down keeps its claim, so the store never runs two threads at once.
|
|
57
|
+
*/
|
|
58
|
+
export declare function convertWithWorker(spawn?: (source: ArrayBuffer) => IconWorker, deadlineMs?: number): AppIconConvert;
|
|
59
|
+
export declare const convertInWorker: AppIconConvert;
|
|
60
|
+
/** The conversion on this thread (tests, tools): the same code, without the worker. */
|
|
61
|
+
export declare const convertInProcess: AppIconConvert;
|
|
62
|
+
/** The sizes made from one decoded source. */
|
|
63
|
+
export declare function appIconSet(bytes: Uint8Array): Record<AppIconFile, Buffer>;
|
|
64
|
+
export declare class AppIconStore {
|
|
65
|
+
private readonly dataDir;
|
|
66
|
+
private readonly now;
|
|
67
|
+
private readonly convert;
|
|
68
|
+
private cached;
|
|
69
|
+
/** One conversion at a time: a second upload meanwhile is told to wait (409). */
|
|
70
|
+
private converting;
|
|
71
|
+
private closed;
|
|
72
|
+
/** Moved by every reset: an icon prepared before it is no longer the person's latest wish, and is not used. */
|
|
73
|
+
private epoch;
|
|
74
|
+
constructor(dataDir: string, now?: () => Date, convert?: AppIconConvert);
|
|
75
|
+
/** The fleet is stopping: nothing more is converted or switched to. */
|
|
76
|
+
close(): void;
|
|
77
|
+
get isClosed(): boolean;
|
|
78
|
+
private get root();
|
|
79
|
+
private get pointer();
|
|
80
|
+
/** The custom icon in force, or null for the bundled one. Read again only when the pointer file changed. */
|
|
81
|
+
current(): AppIconState | null;
|
|
82
|
+
/** A made file of the icon in force (or of `version`), or null. */
|
|
83
|
+
file(name: AppIconFile, version?: string): Buffer | null;
|
|
84
|
+
/**
|
|
85
|
+
* Check a source (its size and magic here; decoding and the sizes in the conversion, off the event loop) and make its
|
|
86
|
+
* files. Nothing on disk changes: commit() switches to them, after its caller checked again who asks.
|
|
87
|
+
*/
|
|
88
|
+
prepare(bytes: Uint8Array): Promise<AppIconPrepareResult>;
|
|
89
|
+
/** Switch to a prepared icon: its files under its version, then the pointer (last, atomically), then old versions go. */
|
|
90
|
+
commit(prepared: AppIconPrepared, by: string): AppIconResult;
|
|
91
|
+
/** prepare() then commit(), for a caller with nothing to check in between. */
|
|
92
|
+
set(bytes: Uint8Array, by: string): Promise<AppIconResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Back to the bundled icon: the pointer removed (none there is already the default), then the files. A pointer that
|
|
95
|
+
* cannot be removed is a failure — nothing is pruned, so the icon in force keeps its files (#2308 review).
|
|
96
|
+
*/
|
|
97
|
+
reset(): {
|
|
98
|
+
ok: true;
|
|
99
|
+
} | {
|
|
100
|
+
ok: false;
|
|
101
|
+
status: 500;
|
|
102
|
+
error: string;
|
|
103
|
+
detail?: string;
|
|
104
|
+
};
|
|
105
|
+
/** Remove every version folder but `keep`. */
|
|
106
|
+
private prune;
|
|
107
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2308: the person's own app icon. One square PNG (the admin's page crops and draws whatever they chose into one) is
|
|
3
|
+
* checked and decoded here (web-png.ts), and made into the sizes a browser and a home screen ask for: the touch icon
|
|
4
|
+
* (180, on white — iOS shows transparency as black), the favicon (an ICO of 32 and 48) and the manifest's 192 and 512.
|
|
5
|
+
*
|
|
6
|
+
* Under the state dir: `web-app-icon/<version>/…`, the version being the source's sha256 (16 hex), and
|
|
7
|
+
* `web-app-icon/current.json` naming the version in force — written last, atomically, so a reader sees the old set or
|
|
8
|
+
* the new one, never half of one. Older version folders are removed once the pointer has moved.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { Worker } from "node:worker_threads";
|
|
12
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { decodePng, encodeIco, encodePng, flatten, isPng, PngError, resize } from "./web-png.js";
|
|
15
|
+
/** What an upload may weigh (the route stops reading past it). */
|
|
16
|
+
export const APP_ICON_MAX_BYTES = 5 * 1024 * 1024;
|
|
17
|
+
/** The source's side: at least the manifest's largest icon, at most what decodes in a few MB of memory. */
|
|
18
|
+
export const APP_ICON_MIN_SIDE = 512;
|
|
19
|
+
export const APP_ICON_MAX_SIDE = 2048;
|
|
20
|
+
/** The files made from the source, by the name each is served as. */
|
|
21
|
+
export const APP_ICON_FILES = ["apple-touch-icon.png", "favicon.ico", "icon-192.png", "icon-512.png"];
|
|
22
|
+
/** The conversion's bounds off the event loop: its time, and its worker's heap. */
|
|
23
|
+
export const APP_ICON_CONVERT_MS = 30_000;
|
|
24
|
+
const CONVERT_HEAP_MB = 256;
|
|
25
|
+
/** The fleet's worker: its own heap; TypeScript sources through tsx, the built .js otherwise. */
|
|
26
|
+
export function spawnIconWorker(source) {
|
|
27
|
+
return import.meta.url.endsWith(".ts")
|
|
28
|
+
? new Worker(`const { workerData } = require("node:worker_threads"); import("tsx/esm/api").then(({ tsImport }) => tsImport(workerData.entry, workerData.entry));`, { eval: true, execArgv: [], resourceLimits: { maxOldGenerationSizeMb: CONVERT_HEAP_MB }, workerData: { entry: new URL("./web-app-icon-worker.ts", import.meta.url).href, source } })
|
|
29
|
+
: new Worker(new URL("./web-app-icon-worker.js", import.meta.url), { execArgv: [], resourceLimits: { maxOldGenerationSizeMb: CONVERT_HEAP_MB }, workerData: { source }, transferList: [source] });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The conversion in a one-off worker thread: its own heap, a deadline (terminated past it), never the fleet's loop.
|
|
33
|
+
* It settles only once the worker has exited (#2319 review): a timed-out or finished conversion whose thread is still
|
|
34
|
+
* winding down keeps its claim, so the store never runs two threads at once.
|
|
35
|
+
*/
|
|
36
|
+
export function convertWithWorker(spawn = spawnIconWorker, deadlineMs = APP_ICON_CONVERT_MS) {
|
|
37
|
+
return (bytes) => new Promise((resolve) => {
|
|
38
|
+
const worker = spawn(new Uint8Array(bytes).buffer);
|
|
39
|
+
let result = null;
|
|
40
|
+
let exited = false;
|
|
41
|
+
const settle = () => { if (result && exited)
|
|
42
|
+
resolve(result); };
|
|
43
|
+
const finish = (r) => {
|
|
44
|
+
if (result)
|
|
45
|
+
return;
|
|
46
|
+
result = r;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
if (!exited)
|
|
49
|
+
void worker.terminate().catch(() => { });
|
|
50
|
+
settle();
|
|
51
|
+
};
|
|
52
|
+
const timer = setTimeout(() => finish({ ok: false, detail: "it took too long to read" }), deadlineMs);
|
|
53
|
+
worker.on("message", (m) => {
|
|
54
|
+
if (!m || !m.ok || !m.files) {
|
|
55
|
+
finish({ ok: false, detail: (m && m.detail) || "the image could not be read" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const files = {};
|
|
59
|
+
for (const name of APP_ICON_FILES) {
|
|
60
|
+
const f = m.files[name];
|
|
61
|
+
if (!f) {
|
|
62
|
+
finish({ ok: false, detail: "the image could not be read" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
files[name] = Buffer.from(f.buffer, f.byteOffset, f.byteLength);
|
|
66
|
+
}
|
|
67
|
+
finish({ ok: true, files });
|
|
68
|
+
});
|
|
69
|
+
worker.on("error", () => finish({ ok: false, detail: "the image could not be read" }));
|
|
70
|
+
worker.on("exit", () => { exited = true; finish({ ok: false, detail: "the image could not be read" }); settle(); });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export const convertInWorker = convertWithWorker();
|
|
74
|
+
/** The conversion on this thread (tests, tools): the same code, without the worker. */
|
|
75
|
+
export const convertInProcess = async (bytes) => {
|
|
76
|
+
try {
|
|
77
|
+
return { ok: true, files: appIconSet(bytes) };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { ok: false, detail: err instanceof PngError ? err.message : "the image could not be read" };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function writeBytesAtomic(path, bytes) {
|
|
84
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
85
|
+
try {
|
|
86
|
+
writeFileSync(tmp, bytes, { mode: 0o600 });
|
|
87
|
+
renameSync(tmp, path);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
try {
|
|
91
|
+
unlinkSync(tmp);
|
|
92
|
+
}
|
|
93
|
+
catch { /* none */ }
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** The sizes made from one decoded source. */
|
|
98
|
+
export function appIconSet(bytes) {
|
|
99
|
+
const src = decodePng(bytes, { maxSide: APP_ICON_MAX_SIDE });
|
|
100
|
+
if (src.width !== src.height)
|
|
101
|
+
throw new PngError(`not square (${src.width}×${src.height})`);
|
|
102
|
+
if (src.width < APP_ICON_MIN_SIDE)
|
|
103
|
+
throw new PngError(`smaller than ${APP_ICON_MIN_SIDE}×${APP_ICON_MIN_SIDE}`);
|
|
104
|
+
const at = (n) => resize(src, n, n);
|
|
105
|
+
return {
|
|
106
|
+
"apple-touch-icon.png": encodePng(flatten(at(180), [255, 255, 255])),
|
|
107
|
+
"favicon.ico": encodeIco([{ size: 32, png: encodePng(at(32)) }, { size: 48, png: encodePng(at(48)) }]),
|
|
108
|
+
"icon-192.png": encodePng(at(192)),
|
|
109
|
+
"icon-512.png": encodePng(at(512)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export class AppIconStore {
|
|
113
|
+
dataDir;
|
|
114
|
+
now;
|
|
115
|
+
convert;
|
|
116
|
+
cached = null;
|
|
117
|
+
/** One conversion at a time: a second upload meanwhile is told to wait (409). */
|
|
118
|
+
converting = false;
|
|
119
|
+
closed = false;
|
|
120
|
+
/** Moved by every reset: an icon prepared before it is no longer the person's latest wish, and is not used. */
|
|
121
|
+
epoch = 0;
|
|
122
|
+
constructor(dataDir, now = () => new Date(), convert = convertInWorker) {
|
|
123
|
+
this.dataDir = dataDir;
|
|
124
|
+
this.now = now;
|
|
125
|
+
this.convert = convert;
|
|
126
|
+
}
|
|
127
|
+
/** The fleet is stopping: nothing more is converted or switched to. */
|
|
128
|
+
close() { this.closed = true; }
|
|
129
|
+
get isClosed() { return this.closed; }
|
|
130
|
+
get root() { return join(this.dataDir, "web-app-icon"); }
|
|
131
|
+
get pointer() { return join(this.root, "current.json"); }
|
|
132
|
+
/** The custom icon in force, or null for the bundled one. Read again only when the pointer file changed. */
|
|
133
|
+
current() {
|
|
134
|
+
let mtimeMs;
|
|
135
|
+
try {
|
|
136
|
+
mtimeMs = statSync(this.pointer).mtimeMs;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
this.cached = { mtimeMs: -1, state: null };
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (this.cached && this.cached.mtimeMs === mtimeMs)
|
|
143
|
+
return this.cached.state;
|
|
144
|
+
let state = null;
|
|
145
|
+
try {
|
|
146
|
+
const v = JSON.parse(readFileSync(this.pointer, "utf8"));
|
|
147
|
+
if (typeof v.version === "string" && /^[0-9a-f]{16}$/.test(v.version) && typeof v.updatedAt === "string" && typeof v.by === "string") {
|
|
148
|
+
state = { version: v.version, updatedAt: v.updatedAt, by: v.by };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch { /* unreadable: the bundled icon */ }
|
|
152
|
+
this.cached = { mtimeMs, state };
|
|
153
|
+
return state;
|
|
154
|
+
}
|
|
155
|
+
/** A made file of the icon in force (or of `version`), or null. */
|
|
156
|
+
file(name, version) {
|
|
157
|
+
const v = version ?? this.current()?.version;
|
|
158
|
+
if (!v || !/^[0-9a-f]{16}$/.test(v))
|
|
159
|
+
return null;
|
|
160
|
+
try {
|
|
161
|
+
return readFileSync(join(this.root, v, name));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Check a source (its size and magic here; decoding and the sizes in the conversion, off the event loop) and make its
|
|
169
|
+
* files. Nothing on disk changes: commit() switches to them, after its caller checked again who asks.
|
|
170
|
+
*/
|
|
171
|
+
async prepare(bytes) {
|
|
172
|
+
if (this.closed)
|
|
173
|
+
return { ok: false, status: 503, error: "app_icon_unavailable" };
|
|
174
|
+
if (bytes.length > APP_ICON_MAX_BYTES)
|
|
175
|
+
return { ok: false, status: 413, error: "app_icon_too_large", detail: "at most 5 MiB" };
|
|
176
|
+
if (!isPng(bytes))
|
|
177
|
+
return { ok: false, status: 415, error: "app_icon_not_png", detail: "a PNG image (the page converts JPG and WebP)" };
|
|
178
|
+
if (this.converting)
|
|
179
|
+
return { ok: false, status: 409, error: "app_icon_busy", detail: "another icon is being made" };
|
|
180
|
+
this.converting = true;
|
|
181
|
+
const epoch = this.epoch;
|
|
182
|
+
try {
|
|
183
|
+
const made = await this.convert(bytes);
|
|
184
|
+
if (!made.ok)
|
|
185
|
+
return { ok: false, status: 400, error: "app_icon_unreadable", detail: made.detail };
|
|
186
|
+
return { ok: true, prepared: { version: createHash("sha256").update(bytes).digest("hex").slice(0, 16), source: bytes, files: made.files, epoch } };
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
this.converting = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Switch to a prepared icon: its files under its version, then the pointer (last, atomically), then old versions go. */
|
|
193
|
+
commit(prepared, by) {
|
|
194
|
+
if (this.closed)
|
|
195
|
+
return { ok: false, status: 503, error: "app_icon_unavailable" };
|
|
196
|
+
// #2319 review: restored to the default while this one was being made — the later wish wins.
|
|
197
|
+
if (prepared.epoch !== this.epoch)
|
|
198
|
+
return { ok: false, status: 409, error: "app_icon_superseded", detail: "the icon was reset meanwhile" };
|
|
199
|
+
const { version } = prepared;
|
|
200
|
+
const state = { version, updatedAt: this.now().toISOString(), by };
|
|
201
|
+
try {
|
|
202
|
+
const dir = join(this.root, version);
|
|
203
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
204
|
+
writeBytesAtomic(join(dir, "source.png"), prepared.source);
|
|
205
|
+
for (const name of APP_ICON_FILES)
|
|
206
|
+
writeBytesAtomic(join(dir, name), prepared.files[name]);
|
|
207
|
+
writeBytesAtomic(this.pointer, Buffer.from(JSON.stringify(state)));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { ok: false, status: 500, error: "app_icon_not_saved" };
|
|
211
|
+
}
|
|
212
|
+
this.cached = null;
|
|
213
|
+
this.prune(version);
|
|
214
|
+
return { ok: true, state };
|
|
215
|
+
}
|
|
216
|
+
/** prepare() then commit(), for a caller with nothing to check in between. */
|
|
217
|
+
async set(bytes, by) {
|
|
218
|
+
const p = await this.prepare(bytes);
|
|
219
|
+
return p.ok ? this.commit(p.prepared, by) : p;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Back to the bundled icon: the pointer removed (none there is already the default), then the files. A pointer that
|
|
223
|
+
* cannot be removed is a failure — nothing is pruned, so the icon in force keeps its files (#2308 review).
|
|
224
|
+
*/
|
|
225
|
+
reset() {
|
|
226
|
+
this.epoch++;
|
|
227
|
+
try {
|
|
228
|
+
unlinkSync(this.pointer);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
if (err.code !== "ENOENT")
|
|
232
|
+
return { ok: false, status: 500, error: "app_icon_not_reset", detail: err.code ?? "error" };
|
|
233
|
+
}
|
|
234
|
+
this.cached = null;
|
|
235
|
+
this.prune(null);
|
|
236
|
+
return { ok: true };
|
|
237
|
+
}
|
|
238
|
+
/** Remove every version folder but `keep`. */
|
|
239
|
+
prune(keep) {
|
|
240
|
+
let names;
|
|
241
|
+
try {
|
|
242
|
+
names = readdirSync(this.root);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const n of names)
|
|
248
|
+
if (/^[0-9a-f]{16}$/.test(n) && n !== keep) {
|
|
249
|
+
try {
|
|
250
|
+
rmSync(join(this.root, n), { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
catch { /* next time */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=web-app-icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-app-icon.js","sourceRoot":"","sources":["../src/web-app-icon.ts"],"names":[],"mappings":"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"}
|
|
@@ -101,6 +101,11 @@ export interface WebChatMessage {
|
|
|
101
101
|
* row with files and no text. A person's messages only.
|
|
102
102
|
*/
|
|
103
103
|
replyTo?: WebChatReplyRef;
|
|
104
|
+
/**
|
|
105
|
+
* #2305: the instance's reactions to this message — its minimal reply ("OK" answered with 👍), as on the platforms.
|
|
106
|
+
* Distinct emojis of the web set (web-reactions.ts), at most WEB_REACTIONS_MAX, in the order they were added.
|
|
107
|
+
*/
|
|
108
|
+
reactions?: string[];
|
|
104
109
|
}
|
|
105
110
|
/** #2272: a reply's reference to an earlier message of the same chat (see WebChatMessage.replyTo). */
|
|
106
111
|
export interface WebChatReplyRef {
|
|
@@ -171,6 +176,8 @@ export interface StoredWebChatMessage {
|
|
|
171
176
|
seenSeq?: number;
|
|
172
177
|
/** #2272: what a person's reply quotes (see WebChatMessage). */
|
|
173
178
|
replyTo?: WebChatReplyRef;
|
|
179
|
+
/** #2305: the instance's reactions (see WebChatMessage). */
|
|
180
|
+
reactions?: string[];
|
|
174
181
|
}
|
|
175
182
|
/** #1565: what the history needs of the file ledger (web-upload.ts). */
|
|
176
183
|
export interface WebChatFiles {
|
|
@@ -282,6 +289,22 @@ export declare class WebChatHistory {
|
|
|
282
289
|
* #1920: a web message's channel copy was sent — it is now what the channel saw. Returns the message when this changed
|
|
283
290
|
* it (the caller tells the guests), null otherwise.
|
|
284
291
|
*/
|
|
292
|
+
/**
|
|
293
|
+
* #2305: the instance reacted to one of its web chat's messages (by the id it was handed). Returns the message and
|
|
294
|
+
* whether this added the emoji (an emoji it already has, or a full list, changes nothing); null when the chat has
|
|
295
|
+
* no such message. `emoji` is already the web set's spelling (web-reactions.ts webReactionEmoji).
|
|
296
|
+
*/
|
|
297
|
+
addReaction(instance: string, messageId: string, emoji: string): {
|
|
298
|
+
message: WebChatMessage;
|
|
299
|
+
added: boolean;
|
|
300
|
+
} | null;
|
|
301
|
+
/** #2305: every kept message's reactions (what a poll sends, as it sends the ticks): its row, and the list. */
|
|
302
|
+
reactionStates(): Array<{
|
|
303
|
+
instance: string;
|
|
304
|
+
boot: string;
|
|
305
|
+
id: number;
|
|
306
|
+
reactions: string[];
|
|
307
|
+
}>;
|
|
285
308
|
markChannel(instance: string, messageId: string): WebChatMessage | null;
|
|
286
309
|
/**
|
|
287
310
|
* #1920 (#2085 review): the messages that became the channel's after this cursor (same boot) — what a guest's poll
|
package/dist/web-chat-history.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { randomBytes } from "node:crypto";
|
|
22
22
|
import { effectTrialOffer } from "./web-effect-trials.js";
|
|
23
23
|
import { webTextLinks } from "./topic-link.js";
|
|
24
|
+
import { WEB_REACTIONS_MAX, webReactionsOf } from "./web-reactions.js";
|
|
24
25
|
/**
|
|
25
26
|
* Ranks for the order a message moves through. Events can arrive late or twice (a queued after the spawn
|
|
26
27
|
* settles, a retry): a tick never moves back. `delivered` and `failed` are final; `cancelled` is not, since
|
|
@@ -166,7 +167,7 @@ export class WebChatHistory {
|
|
|
166
167
|
stored(m) {
|
|
167
168
|
const row = { boot: m.boot, id: m.id, instance: m.instance, role: m.role, sender: m.sender, text: m.text, ts: m.ts, ...(m.click ? { click: { label: m.click.label } } : {}), ...(m.links ? { links: m.links } : {}),
|
|
168
169
|
...(m.channel ? { channel: true } : {}), ...(m.from ? { from: m.from } : {}), ...(m.channel && m.seenSeq !== undefined ? { seenSeq: m.seenSeq } : {}),
|
|
169
|
-
...(m.replyTo ? { replyTo: m.replyTo } : {}) };
|
|
170
|
+
...(m.replyTo ? { replyTo: m.replyTo } : {}), ...(m.reactions?.length ? { reactions: [...m.reactions] } : {}) };
|
|
170
171
|
if (m.attachments?.length) {
|
|
171
172
|
row.attachments = m.attachments.map((a) => {
|
|
172
173
|
const base = { kind: a.kind, name: a.name, size: a.size, mime: a.mime };
|
|
@@ -366,6 +367,31 @@ export class WebChatHistory {
|
|
|
366
367
|
* #1920: a web message's channel copy was sent — it is now what the channel saw. Returns the message when this changed
|
|
367
368
|
* it (the caller tells the guests), null otherwise.
|
|
368
369
|
*/
|
|
370
|
+
/**
|
|
371
|
+
* #2305: the instance reacted to one of its web chat's messages (by the id it was handed). Returns the message and
|
|
372
|
+
* whether this added the emoji (an emoji it already has, or a full list, changes nothing); null when the chat has
|
|
373
|
+
* no such message. `emoji` is already the web set's spelling (web-reactions.ts webReactionEmoji).
|
|
374
|
+
*/
|
|
375
|
+
addReaction(instance, messageId, emoji) {
|
|
376
|
+
const m = this.findByMessageId(instance, messageId);
|
|
377
|
+
if (!m)
|
|
378
|
+
return null;
|
|
379
|
+
const list = m.reactions ?? [];
|
|
380
|
+
if (list.includes(emoji) || list.length >= WEB_REACTIONS_MAX)
|
|
381
|
+
return { message: m, added: false };
|
|
382
|
+
m.reactions = [...list, emoji];
|
|
383
|
+
this.persist(instance);
|
|
384
|
+
return { message: m, added: true };
|
|
385
|
+
}
|
|
386
|
+
/** #2305: every kept message's reactions (what a poll sends, as it sends the ticks): its row, and the list. */
|
|
387
|
+
reactionStates() {
|
|
388
|
+
const out = [];
|
|
389
|
+
for (const [instance, slot] of this.byInstance)
|
|
390
|
+
for (const m of slot.messages)
|
|
391
|
+
if (m.reactions?.length)
|
|
392
|
+
out.push({ instance, boot: m.boot, id: m.id, reactions: [...m.reactions] });
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
369
395
|
markChannel(instance, messageId) {
|
|
370
396
|
const m = this.findByMessageId(instance, messageId);
|
|
371
397
|
if (!m || m.channel)
|
|
@@ -556,7 +582,8 @@ export class WebChatHistory {
|
|
|
556
582
|
}
|
|
557
583
|
/**
|
|
558
584
|
* #1565: one stored row as a restored message (and its files, to serve again), or null when it is not ours in shape.
|
|
559
|
-
* Only what was said comes back: no buttons, no delivery tick, no message id — whatever a row carries of those.
|
|
585
|
+
* Only what was said comes back: no buttons, no delivery tick, no message id — whatever a row carries of those. #2305:
|
|
586
|
+
* and the instance's reactions (what was answered). With no id, a message from before the restart takes no new one.
|
|
560
587
|
*/
|
|
561
588
|
function restoredRow(raw, instance, thisBoot) {
|
|
562
589
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -588,6 +615,9 @@ function restoredRow(raw, instance, thisBoot) {
|
|
|
588
615
|
const replyTo = role === "user" ? webChatReplyRefOf(r.replyTo) : undefined;
|
|
589
616
|
if (replyTo)
|
|
590
617
|
message.replyTo = replyTo;
|
|
618
|
+
const reactions = webReactionsOf(r.reactions);
|
|
619
|
+
if (reactions)
|
|
620
|
+
message.reactions = reactions;
|
|
591
621
|
const files = !Array.isArray(r.attachments) ? [] : r.attachments.slice(0, 20).flatMap((a) => {
|
|
592
622
|
if (!a || typeof a !== "object" || Array.isArray(a))
|
|
593
623
|
return [];
|