Add voice audio, capture and transcript, browser monitor, and DM UI
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user