Add voice audio, capture and transcript, browser monitor, and DM UI

This commit is contained in:
Fisto
2026-09-14 11:19:06 +02:00
parent 65f0788502
commit c003d9532f
27 changed files with 4479 additions and 239 deletions
+138
View File
@@ -0,0 +1,138 @@
/*
* Veskslop daemon — web UI preferences.
*
* Stored next to the token file, not in the puppet state, so the web UI can
* read and write them while the Discord client is disconnected.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
export interface TtsPreferences {
provider: "local" | "api" | null;
language: string | null;
voice: string | null;
}
export interface FavouritesPreferences {
users: string[];
channels: string[];
}
export interface WebPreferences {
/** null means every guild is visible. An empty array hides all. */
visibleGuilds: string[] | null;
tts: TtsPreferences;
favourites: FavouritesPreferences;
}
const DEFAULTS: WebPreferences = {
visibleGuilds: null,
tts: { provider: null, language: null, voice: null },
favourites: { users: [], channels: [] }
};
export interface PreferencesOptions {
/** The old state file that may still hold preferences. */
legacyFile?: string;
}
export class PreferencesStore {
private preferences: WebPreferences = structuredClone(DEFAULTS);
constructor(
private readonly file = process.env.VESKSLOP_PREFERENCES ?? ".veskslop-preferences.json",
options: PreferencesOptions = {}
) {
try {
if (existsSync(this.file)) {
this.preferences = this.normalize(JSON.parse(readFileSync(this.file, "utf8")));
} else {
this.migrate(options.legacyFile);
}
} catch (error) {
console.warn("[preferences] could not read", this.file, (error as Error).message);
}
}
get(): WebPreferences {
return structuredClone(this.preferences);
}
update(patch: Partial<WebPreferences>): WebPreferences {
if (patch.visibleGuilds !== undefined) {
this.preferences.visibleGuilds = Array.isArray(patch.visibleGuilds)
? [...new Set(patch.visibleGuilds.map(String))]
: null;
}
if (patch.tts !== undefined) {
this.preferences.tts = {
provider: patch.tts?.provider === "api" ? "api" : patch.tts?.provider === "local" ? "local" : null,
language: patch.tts?.language ? String(patch.tts.language).slice(0, 16) : null,
voice: patch.tts?.voice ? String(patch.tts.voice).slice(0, 64) : null
};
}
if (patch.favourites !== undefined) {
this.preferences.favourites = {
users: Array.isArray(patch.favourites?.users)
? [...new Set(patch.favourites.users.map(String))].slice(0, 100)
: [],
channels: Array.isArray(patch.favourites?.channels)
? [...new Set(patch.favourites.channels.map(String))].slice(0, 100)
: []
};
}
this.save();
return this.get();
}
private migrate(legacyFile?: string) {
if (!legacyFile || !existsSync(legacyFile)) return;
try {
const legacy = JSON.parse(readFileSync(legacyFile, "utf8"));
if (legacy?.preferences) {
this.preferences = this.normalize(legacy.preferences);
this.save();
console.log("[preferences] migrated from", legacyFile);
}
} catch {
// nothing to migrate
}
}
private normalize(raw: any): WebPreferences {
const provider = raw?.tts?.provider;
return {
visibleGuilds: Array.isArray(raw?.visibleGuilds)
? [...new Set<string>(raw.visibleGuilds.map((id: unknown) => String(id)))]
: null,
tts: {
provider: provider === "api" || provider === "local" ? provider : null,
language: raw?.tts?.language ? String(raw.tts.language).slice(0, 16) : null,
voice: raw?.tts?.voice ? String(raw.tts.voice).slice(0, 64) : null
},
favourites: {
users: Array.isArray(raw?.favourites?.users)
? [...new Set<string>(raw.favourites.users.map((id: unknown) => String(id)))].slice(0, 100)
: [],
channels: Array.isArray(raw?.favourites?.channels)
? [...new Set<string>(raw.favourites.channels.map((id: unknown) => String(id)))].slice(0, 100)
: []
}
};
}
private save() {
try {
mkdirSync(dirname(this.file), { recursive: true });
writeFileSync(this.file, JSON.stringify(this.preferences, null, 4));
} catch (error) {
console.error("[preferences] write failed:", error);
}
}
}