Add MicroslopSam TTS, Flextalk voice profiles, effects, and engine controls
This commit is contained in:
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { openPrivateChannel, sendMessage } from "@vencord/types/utils";
|
||||
import { findByPropsLazy } from "@vencord/types/webpack";
|
||||
import { findByPropsLazy, findStoreLazy } from "@vencord/types/webpack";
|
||||
import {
|
||||
ChannelRTCStore,
|
||||
ChannelStore,
|
||||
GuildMemberStore,
|
||||
GuildStore,
|
||||
@@ -45,6 +46,63 @@ const ChannelActions = findByPropsLazy("selectVoiceChannel");
|
||||
/** Discord's media engine actions, located lazily at runtime. */
|
||||
const MediaEngineActions = findByPropsLazy("setSelfMute");
|
||||
|
||||
/** Per-user volume store, located lazily at runtime. */
|
||||
const UserVolumeStore = findStoreLazy("UserVolumeStore");
|
||||
|
||||
/** Discord's per-user volume actions, located lazily at runtime. */
|
||||
const UserVolumeActions = findByPropsLazy("setUserVolume");
|
||||
|
||||
/** Media engine store, for the client's own output volume. */
|
||||
const MediaEngineStore = findStoreLazy("MediaEngineStore");
|
||||
|
||||
function userVolumeOf(userId: string): number | null {
|
||||
const candidates: any[] = [UserVolumeStore, UserVolumeActions, MediaEngineStore, MediaEngineActions];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const getter = candidate?.getUserVolume;
|
||||
if (typeof getter !== "function") continue;
|
||||
|
||||
const raw = getter.call(candidate, userId);
|
||||
const value = typeof raw === "object" && raw !== null ? (raw as any).volume : raw;
|
||||
const volume = Number(value);
|
||||
if (Number.isFinite(volume)) return volume;
|
||||
} catch {
|
||||
// The lazy proxy throws when the module is absent.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function userVolumeSetter(): ((userId: string, volume: number) => void) | undefined {
|
||||
const candidates: any[] = [UserVolumeStore, UserVolumeActions, MediaEngineStore, MediaEngineActions];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const setter = candidate?.setUserVolume;
|
||||
if (typeof setter === "function") return setter.bind(candidate);
|
||||
} catch {
|
||||
// The lazy proxy throws when the module is absent.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function outputVolume(): number | null {
|
||||
try {
|
||||
const store = MediaEngineStore as any;
|
||||
if (typeof store?.getLocalVolume === "function") {
|
||||
const volume = Number(store.getLocalVolume());
|
||||
return Number.isFinite(volume) ? volume : null;
|
||||
}
|
||||
} catch {
|
||||
// Not available in this client build.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function compareIds(a: string, b: string): number {
|
||||
try {
|
||||
const left = BigInt(a);
|
||||
@@ -193,7 +251,8 @@ function voiceMember(state: any, guildId?: string) {
|
||||
selfDeaf: !!state.selfDeaf,
|
||||
video: !!state.selfVideo,
|
||||
stream: !!state.selfStream || !!state.stream,
|
||||
suppress: !!state.suppress
|
||||
suppress: !!state.suppress,
|
||||
volume: userVolumeOf(state.userId)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -373,6 +432,7 @@ export async function handlePuppetAction(action: string, params: Record<string,
|
||||
guildId: channel?.guild_id ?? guildId ?? null,
|
||||
channelId: voiceState.channelId,
|
||||
channelName: channel?.name ?? null,
|
||||
outputVolume: outputVolume(),
|
||||
members: Object.values(states).map(state =>
|
||||
voiceMember(state, (channel?.guild_id as string | undefined) ?? (guildId as string | undefined))
|
||||
)
|
||||
@@ -436,6 +496,72 @@ export async function handlePuppetAction(action: string, params: Record<string,
|
||||
});
|
||||
}
|
||||
|
||||
case PuppetAction.VoiceSpeaking: {
|
||||
const targetId = UserStore.getCurrentUser()?.id;
|
||||
assert(targetId, PuppetErrorCode.NotLoggedIn, "no user is logged in");
|
||||
|
||||
const voiceState =
|
||||
VoiceStateStore.getVoiceStateForUser(targetId) ?? VoiceStateStore.getVoiceState(null, targetId);
|
||||
|
||||
if (!voiceState?.channelId) return ok({ channelId: null, userIds: [] });
|
||||
|
||||
const userIds = ChannelRTCStore.getSpeakingParticipants(voiceState.channelId)
|
||||
.filter(participant => participant.speaking)
|
||||
.map(participant => participant.user?.id ?? participant.id)
|
||||
.filter(Boolean);
|
||||
|
||||
return ok({ channelId: voiceState.channelId, userIds });
|
||||
}
|
||||
|
||||
case PuppetAction.UserVolume: {
|
||||
const { userIds } = params;
|
||||
assert(
|
||||
Array.isArray(userIds) && userIds.length >= 1 && userIds.length <= 50,
|
||||
PuppetErrorCode.InvalidParam,
|
||||
"userIds must be an array of 1 to 50 user IDs"
|
||||
);
|
||||
assert(
|
||||
(userIds as string[]).every(id => isSnowflake(id)),
|
||||
PuppetErrorCode.InvalidSnowflake,
|
||||
"every user ID must be a snowflake"
|
||||
);
|
||||
|
||||
return ok({
|
||||
outputVolume: outputVolume(),
|
||||
users: (userIds as string[]).map(id => ({ id, volume: userVolumeOf(id) }))
|
||||
});
|
||||
}
|
||||
|
||||
case PuppetAction.UserSetVolume: {
|
||||
const { userId, volume } = params;
|
||||
assert(isSnowflake(userId), PuppetErrorCode.InvalidSnowflake, "userId must be a snowflake");
|
||||
assert(
|
||||
typeof volume === "number" && Number.isFinite(volume) && volume >= 0 && volume <= 200,
|
||||
PuppetErrorCode.InvalidParam,
|
||||
"volume must be a number from 0 to 200"
|
||||
);
|
||||
|
||||
const setUserVolume = userVolumeSetter();
|
||||
assert(
|
||||
typeof setUserVolume === "function",
|
||||
PuppetErrorCode.NotSupported,
|
||||
"per-user volume is not available in this client build"
|
||||
);
|
||||
|
||||
enforceOutboundRateLimit();
|
||||
|
||||
try {
|
||||
setUserVolume(userId, volume);
|
||||
} catch (error) {
|
||||
throw new ActionError(
|
||||
PuppetErrorCode.VoiceFailed,
|
||||
`setting the user volume failed: ${String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
return ok({ userId, volume, outputVolume: outputVolume() });
|
||||
}
|
||||
|
||||
case PuppetAction.VoiceOverview: {
|
||||
const { guildId } = params;
|
||||
assert(isSnowflake(guildId), PuppetErrorCode.InvalidSnowflake, "guildId must be a snowflake");
|
||||
@@ -489,6 +615,18 @@ export async function handlePuppetAction(action: string, params: Record<string,
|
||||
}
|
||||
}
|
||||
|
||||
// Discord persists self-deafen across sessions. A deafened
|
||||
// client receives no call audio, which breaks the monitor and
|
||||
// the transcript, so joining clears it.
|
||||
const setSelfDeaf = (MediaEngineActions as any)?.setSelfDeaf;
|
||||
if (typeof setSelfDeaf === "function") {
|
||||
try {
|
||||
setSelfDeaf(false);
|
||||
} catch {
|
||||
// Best effort: the join still works.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await selectVoiceChannel(channelId);
|
||||
} catch (error) {
|
||||
@@ -791,7 +929,8 @@ export async function handlePuppetAction(action: string, params: Record<string,
|
||||
} catch (error) {
|
||||
if (error instanceof ActionError) return fail(error.code, error.message);
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error("[puppet] action failed:", action, error);
|
||||
return fail(PuppetErrorCode.Internal, "action failed");
|
||||
return fail(PuppetErrorCode.Internal, `action failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +217,19 @@ export function initPuppetEvents() {
|
||||
|
||||
dispatcher.subscribe("SPEAKING", (event: any) => {
|
||||
try {
|
||||
if (!event?.userId) return;
|
||||
const userId = event?.userId ?? event?.user_id;
|
||||
if (!userId) return;
|
||||
|
||||
// Discord sends a number bitfield (1 = microphone). A missing
|
||||
// flag is treated as a start pulse: some dispatcher payloads
|
||||
// carry no flag, and the UI decays on its own after silence.
|
||||
const raw = event.speaking ?? event.speakingFlag;
|
||||
const speaking = raw === undefined ? true : typeof raw === "boolean" ? raw : Number(raw) !== 0;
|
||||
|
||||
VesktopNative.puppet.emit({
|
||||
type: "VOICE_SPEAKING",
|
||||
userId: event.userId,
|
||||
speaking: event.speaking !== false,
|
||||
userId,
|
||||
speaking,
|
||||
channelId: event.channelId ?? event.channel_id ?? null
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user