Guard Metame voice on human listeners
This commit is contained in:
parent
7dbe3efb4b
commit
f338eb4854
3 changed files with 47 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
type RepoDiscordIdentityRegistry,
|
||||
WeksaSpeechClient,
|
||||
} from "@voidbot/core";
|
||||
import { ChannelType } from "discord.js";
|
||||
import type { Message } from "discord.js";
|
||||
|
||||
const METAME_VOICE_DESIGN_PROMPT = [
|
||||
|
|
@ -53,6 +54,12 @@ export async function maybeMirrorOwnerAquariumMessageAsMetameVoice(input: {
|
|||
console.warn(`Skipped Metame owner voice bridge: ${metame.id} is not allowed in channel ${message.channelId}.`);
|
||||
return;
|
||||
}
|
||||
if (!(await hasHumanListenerInAquariumVoice(message, config.repoFaceDiscordVoice.channelId))) {
|
||||
console.log(
|
||||
`Skipped Metame owner voice bridge for message ${message.id}: no human listeners in configured Aquarium voice channel.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await appendMetameOwnerVoiceState({
|
||||
statePath: config.metameOwnerVoice.statePath,
|
||||
|
|
@ -170,3 +177,23 @@ async function appendMetameOwnerVoiceState(input: {
|
|||
].join("\n");
|
||||
await appendFile(input.statePath, `${header}${entry}`, "utf8");
|
||||
}
|
||||
|
||||
async function hasHumanListenerInAquariumVoice(
|
||||
message: Message,
|
||||
voiceChannelId: string | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!message.guild || !voiceChannelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const channel = await message.guild.channels.fetch(voiceChannelId).catch((error) => {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Could not inspect Aquarium voice channel ${voiceChannelId}: ${detail}`);
|
||||
return null;
|
||||
});
|
||||
if (!channel || (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return channel.members.some((member) => !member.user.bot);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
type AudioPlayer,
|
||||
type VoiceConnection,
|
||||
} from "@discordjs/voice";
|
||||
import { ChannelType, type Client, type GuildBasedChannel } from "discord.js";
|
||||
import { ChannelType, type Client, type VoiceBasedChannel } from "discord.js";
|
||||
|
||||
import {
|
||||
appendRepoFaceVoicePlayedEntry,
|
||||
|
|
@ -88,6 +88,12 @@ class RepoFaceVoicePlaybackRuntime {
|
|||
this.active = true;
|
||||
try {
|
||||
const channel = await this.resolveVoiceChannel();
|
||||
if (!hasHumanListener(channel)) {
|
||||
console.log(
|
||||
`Skipped repo Face voice playback for ${entry.id}: no human listeners in channel ${this.options.channelId}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const connection = await this.ensureConnection(channel);
|
||||
const resource = createAudioResource(entry.audioPath, {
|
||||
metadata: entry,
|
||||
|
|
@ -108,7 +114,7 @@ class RepoFaceVoicePlaybackRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
private async resolveVoiceChannel(): Promise<GuildBasedChannel> {
|
||||
private async resolveVoiceChannel(): Promise<VoiceBasedChannel> {
|
||||
const channel = await this.client.channels.fetch(this.options.channelId!);
|
||||
if (!channel || !("guild" in channel)) {
|
||||
throw new Error(`Configured voice channel ${this.options.channelId} was not found in a guild.`);
|
||||
|
|
@ -119,7 +125,7 @@ class RepoFaceVoicePlaybackRuntime {
|
|||
return channel;
|
||||
}
|
||||
|
||||
private async ensureConnection(channel: GuildBasedChannel): Promise<VoiceConnection> {
|
||||
private async ensureConnection(channel: VoiceBasedChannel): Promise<VoiceConnection> {
|
||||
if (
|
||||
this.connection &&
|
||||
this.connection.joinConfig.channelId === channel.id &&
|
||||
|
|
@ -136,7 +142,17 @@ class RepoFaceVoicePlaybackRuntime {
|
|||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||
selfDeaf: false,
|
||||
});
|
||||
this.connection.on("error", (error) => {
|
||||
console.warn(`Repo Face voice connection error in channel ${channel.id}: ${error.message}`);
|
||||
this.connection?.destroy();
|
||||
this.connection = undefined;
|
||||
this.active = false;
|
||||
});
|
||||
await entersState(this.connection, VoiceConnectionStatus.Ready, 30_000);
|
||||
return this.connection;
|
||||
}
|
||||
}
|
||||
|
||||
function hasHumanListener(channel: VoiceBasedChannel): boolean {
|
||||
return channel.members.some((member) => !member.user.bot);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -357,3 +357,4 @@
|
|||
{"ts":"2026-06-12T18:12:19Z","type":"implementation","status":"ok","note":"Added owner-authorized Aquarium-to-Metame voice bridge: owner messages in Aquarium can be mirrored exactly through shared Weksa/MiMo speech rendering, persisted to a Weksa-readable Metame voice-sync projection, and queued into the existing Discord voice outbox."}
|
||||
{"ts":"2026-06-13T08:34:42Z","type":"implementation","status":"ok","note":"Created native Moobs Persona CultCache state at .voidbot/private/personas/moobs/moobs.cc from D:/Downloads/Moobs.txt; Moobs remains outside repo Face/swarm registries and is for iVixey's separate Discord."}
|
||||
{"ts":"2026-06-13T12:09:58Z","type":"verification","status":"ok","note":"2026-06-13 local moderation heartbeat was re-enabled by setting VOIDBOT_MODERATION_HEARTBEAT_ENABLED=true while VOID_MODERATION_ENFORCEMENT_MODE=policy. A direct ModerationHeartbeatOnly pass completed ok in policy mode, observed 0 new messages past cursor 2026-06-12T18:40:31.845Z, proposed/applied 0 operations, and left openCaseCount=0."}
|
||||
{"ts":"2026-06-13T19:11:52Z","type":"implementation","status":"ok","note":"Disabled Metame owner voice locally and hardened the voice path: when re-enabled, owner Aquarium text only renders through Weksa if the configured Aquarium voice channel currently has a non-bot listener; playback also skips empty voice channels and logs connection errors instead of crashing the bot."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue