fix: resolve architecture disconnects and codebase weaknesses
- fix(backend): replace raw .parse() with proper loadConfig() + ConfigError - fix(gateway): connect voice recording uploader to EventBroadcaster - fix(gateway): remove dead globalThis.moderationBroadcaster path in AI analyzer - fix(gateway): eliminate audioStream race condition by attaching handlers before pipe - fix(gateway): enable inlineVolume by default for setMusicVolume to work - fix(gateway): reuse persistent redisPub for command replies (no new connection per cmd) - fix(frontend): add missing voice_active_user/voice_pcm_data to WsEventMap - fix(frontend): correct onAttachmentUploaded handler signature to accept data - chore: move @types/pg from dependencies to devDependencies - chore: translate remaining Indonesian comments to English - chore: remove stale P3 TODO comment
This commit is contained in:
Generated
+3
-3
@@ -52,9 +52,6 @@ importers:
|
|||||||
'@discordjs/voice':
|
'@discordjs/voice':
|
||||||
specifier: ^0.19.2
|
specifier: ^0.19.2
|
||||||
version: 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(opusscript@0.0.8)
|
version: 0.19.2(@discordjs/opus@0.10.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(opusscript@0.0.8)
|
||||||
'@types/pg':
|
|
||||||
specifier: ^8.20.0
|
|
||||||
version: 8.20.0
|
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.16.1
|
specifier: ^1.16.1
|
||||||
version: 1.16.1
|
version: 1.16.1
|
||||||
@@ -101,6 +98,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.9.0
|
specifier: ^25.9.0
|
||||||
version: 25.9.0
|
version: 25.9.0
|
||||||
|
'@types/pg':
|
||||||
|
specifier: ^8.20.0
|
||||||
|
version: 8.20.0
|
||||||
'@types/ws':
|
'@types/ws':
|
||||||
specifier: ^8.18.1
|
specifier: ^8.18.1
|
||||||
version: 8.18.1
|
version: 8.18.1
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bete/shared": "workspace:*",
|
"@bete/shared": "workspace:*",
|
||||||
"@discordjs/voice": "^0.19.2",
|
"@discordjs/voice": "^0.19.2",
|
||||||
"@types/pg": "^8.20.0",
|
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -37,6 +36,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"tsx": "^4.22.2",
|
"tsx": "^4.22.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"vitest": "latest"
|
"vitest": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { ConfigError } from "@bete/shared/errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const configSchema = z
|
const configSchema = z
|
||||||
@@ -86,7 +87,31 @@ const configSchema = z
|
|||||||
.url()
|
.url()
|
||||||
.default("https://upload.asepharyana.my.id/api/upload"),
|
.default("https://upload.asepharyana.my.id/api/upload"),
|
||||||
})
|
})
|
||||||
.parse(process.env);
|
.superRefine((value, ctx) => {
|
||||||
|
if (!value.DATABASE_URL && !value.DATABASE_HOST) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["DATABASE_URL"],
|
||||||
|
message: "Either DATABASE_URL or DATABASE_HOST must be provided",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const config = configSchema;
|
export function loadConfig(
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): z.infer<typeof configSchema> {
|
||||||
|
try {
|
||||||
|
return configSchema.parse(env);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
const messages = error.issues
|
||||||
|
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
||||||
|
.join("\n");
|
||||||
|
throw new ConfigError(`Configuration validation failed:\n${messages}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = loadConfig();
|
||||||
export type Config = typeof config;
|
export type Config = typeof config;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import type {
|
|||||||
AnalysisQueueStatus,
|
AnalysisQueueStatus,
|
||||||
AnalysisResult,
|
AnalysisResult,
|
||||||
MessageRecord,
|
MessageRecord,
|
||||||
ModerationBroadcaster,
|
|
||||||
} from "../message-capture/types.js";
|
} from "../message-capture/types.js";
|
||||||
import { attemptAutoDeleteFlaggedMessage } from "./autoDeleteManager.js";
|
import { attemptAutoDeleteFlaggedMessage } from "./autoDeleteManager.js";
|
||||||
import { estimateTokens } from "./conversationContext.js";
|
import { estimateTokens } from "./conversationContext.js";
|
||||||
@@ -30,22 +29,12 @@ import { logModerationError } from "./responseLogger.js";
|
|||||||
|
|
||||||
const logger = createChildLogger("ai-analyzer");
|
const logger = createChildLogger("ai-analyzer");
|
||||||
|
|
||||||
type ModerationGlobal = typeof globalThis & {
|
|
||||||
moderationBroadcaster?: ModerationBroadcaster;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getModerationBroadcaster(): ModerationBroadcaster | undefined {
|
|
||||||
return (globalThis as ModerationGlobal).moderationBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redis EventBroadcaster — set by startPendingAIAnalysisWorker.
|
// Redis EventBroadcaster — set by startPendingAIAnalysisWorker.
|
||||||
// Used to publish analysis completion events so the backend
|
// Used to publish analysis completion events so the backend
|
||||||
// redis-bridge can forward them to frontend WebSocket clients.
|
// redis-bridge can forward them to frontend WebSocket clients.
|
||||||
let _redisEventBroadcaster: EventBroadcaster | undefined;
|
let _redisEventBroadcaster: EventBroadcaster | undefined;
|
||||||
|
|
||||||
function broadcastAnalysisCompleted(row: MessageRecord): void {
|
function broadcastAnalysisCompleted(row: MessageRecord): void {
|
||||||
// In-memory WS broadcast (direct-connected DG clients)
|
|
||||||
getModerationBroadcaster()?.messageAnalyzed(row);
|
|
||||||
// Redis pub/sub broadcast → backend → frontend WebSocket
|
// Redis pub/sub broadcast → backend → frontend WebSocket
|
||||||
if (_redisEventBroadcaster) {
|
if (_redisEventBroadcaster) {
|
||||||
_redisEventBroadcaster.messageAnalyzed(row).catch((err: unknown) =>
|
_redisEventBroadcaster.messageAnalyzed(row).catch((err: unknown) =>
|
||||||
|
|||||||
@@ -178,12 +178,11 @@ export class CommandHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish reply on the designated reply channel.
|
// Publish reply on the designated reply channel using the persistent publisher.
|
||||||
const redisPub = new Redis(config.REDIS_URL);
|
|
||||||
try {
|
try {
|
||||||
await redisPub.publish(cmd.replyChannel, JSON.stringify(reply));
|
await this.redisPub.publish(cmd.replyChannel, JSON.stringify(reply));
|
||||||
} finally {
|
} catch (err) {
|
||||||
await redisPub.quit();
|
logger.error({ err }, "Failed to publish command reply");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always refresh status keys after every command so the backend has
|
// Always refresh status keys after every command so the backend has
|
||||||
|
|||||||
@@ -257,7 +257,6 @@ export async function getMessagesByChannel(
|
|||||||
.select()
|
.select()
|
||||||
.from(messagesTable)
|
.from(messagesTable)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
// P3: add secondary sort by id for stable pagination
|
|
||||||
.orderBy(desc(messagesTable.created_at), desc(messagesTable.id))
|
.orderBy(desc(messagesTable.created_at), desc(messagesTable.id))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export type DiscordPlayerOwner = "none" | "browser-bridge" | "music" | "screen";
|
|||||||
|
|
||||||
export interface DiscordPlayOptions {
|
export interface DiscordPlayOptions {
|
||||||
inputType?: StreamType;
|
inputType?: StreamType;
|
||||||
|
/** Enable volume control via resource.volume (required for setMusicVolume to work). */
|
||||||
inlineVolume?: boolean;
|
inlineVolume?: boolean;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Transform, TransformCallback } from "node:stream";
|
import { Transform, TransformCallback } from "node:stream";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform stream untuk memfilter audio packets yang terlalu kecil
|
* Transform stream to filter out audio packets that are too small.
|
||||||
* Packet yang terlalu kecil kemungkinan gagal didekripsi oleh Discord
|
* Packets that are too small are likely to fail decryption by Discord.
|
||||||
*/
|
*/
|
||||||
export class PacketFilter extends Transform {
|
export class PacketFilter extends Transform {
|
||||||
private minPacketSize: number;
|
private minPacketSize: number;
|
||||||
@@ -21,7 +21,7 @@ export class PacketFilter extends Transform {
|
|||||||
): void {
|
): void {
|
||||||
this.totalCount++;
|
this.totalCount++;
|
||||||
|
|
||||||
// Filter packet yang terlalu kecil
|
// Filter out undersized packets
|
||||||
if (chunk.length >= this.minPacketSize) {
|
if (chunk.length >= this.minPacketSize) {
|
||||||
this.push(chunk);
|
this.push(chunk);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export class DiscordPlayer {
|
|||||||
|
|
||||||
const resource = createAudioResource(stream, {
|
const resource = createAudioResource(stream, {
|
||||||
inputType: options.inputType ?? StreamType.OggOpus,
|
inputType: options.inputType ?? StreamType.OggOpus,
|
||||||
inlineVolume: options.inlineVolume ?? false,
|
// Default to true so setMusicVolume/setResourceVolume works
|
||||||
|
inlineVolume: options.inlineVolume ?? true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.owner === owner) {
|
if (this.owner === owner) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import fs, { promises as fsPromises } from "node:fs";
|
import { promises as fsPromises } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import { retryWithBackoff } from "@bete/shared/utils";
|
import { retryWithBackoff } from "@bete/shared/utils";
|
||||||
@@ -32,13 +32,16 @@ const logger = createChildLogger("recorder");
|
|||||||
|
|
||||||
let _eventBroadcaster: EventBroadcaster | undefined;
|
let _eventBroadcaster: EventBroadcaster | undefined;
|
||||||
|
|
||||||
|
/** @internal Export for uploader.ts to broadcast voice_recording_uploaded events */
|
||||||
|
export { _eventBroadcaster };
|
||||||
|
|
||||||
export function setEventBroadcaster(broadcaster: EventBroadcaster | undefined) {
|
export function setEventBroadcaster(broadcaster: EventBroadcaster | undefined) {
|
||||||
_eventBroadcaster = broadcaster;
|
_eventBroadcaster = broadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordingsDir = config.RECORDINGS_DIR;
|
const recordingsDir = config.RECORDINGS_DIR;
|
||||||
|
|
||||||
// Pastikan folder recordings ada
|
// Ensure recordings directory exists
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await fsPromises.mkdir(recordingsDir, { recursive: true });
|
await fsPromises.mkdir(recordingsDir, { recursive: true });
|
||||||
@@ -94,7 +97,7 @@ export async function startRecording(
|
|||||||
logger.error({ error: err }, "Voice connection error");
|
logger.error({ error: err }, "Voice connection error");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tunggu sampai benar-benar terhubung dengan retry logic
|
// Wait until fully connected with retry logic
|
||||||
try {
|
try {
|
||||||
await retryWithBackoff(
|
await retryWithBackoff(
|
||||||
() =>
|
() =>
|
||||||
@@ -148,7 +151,7 @@ export async function startRecording(
|
|||||||
speaking: true,
|
speaking: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Jangan record kalau sudah ada stream aktif untuk user ini
|
// Skip if user already has an active stream
|
||||||
if (receiver.subscriptions.has(userId)) return;
|
if (receiver.subscriptions.has(userId)) return;
|
||||||
|
|
||||||
const userDir = path.join(recordingsDir, userId);
|
const userDir = path.join(recordingsDir, userId);
|
||||||
@@ -157,17 +160,19 @@ export async function startRecording(
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- OGG file recording with segment rotation ---
|
// Subscribe to the audio stream FIRST, then immediately attach all event
|
||||||
const packetFilterForOgg = new PacketFilter(
|
// handlers before piping — prevents race condition where initial packets
|
||||||
config.PACKET_FILTER_MIN_SIZE,
|
// arrive before listeners are registered.
|
||||||
);
|
|
||||||
const audioStream = receiver.subscribe(userId, {
|
const audioStream = receiver.subscribe(userId, {
|
||||||
end: {
|
end: {
|
||||||
behavior: EndBehaviorType.AfterSilence,
|
behavior: EndBehaviorType.AfterSilence,
|
||||||
duration: config.AUDIO_STREAM_SILENCE_DURATION_MS,
|
duration: config.AUDIO_STREAM_SILENCE_DURATION_MS,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const oggPacketStream = audioStream.pipe(packetFilterForOgg);
|
|
||||||
|
const packetFilterForOgg = new PacketFilter(
|
||||||
|
config.PACKET_FILTER_MIN_SIZE,
|
||||||
|
);
|
||||||
const segmentManager = new SegmentManager(
|
const segmentManager = new SegmentManager(
|
||||||
userDir,
|
userDir,
|
||||||
config.RECORDING_SEGMENT_MS,
|
config.RECORDING_SEGMENT_MS,
|
||||||
@@ -187,6 +192,33 @@ export async function startRecording(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attach all audioStream event handlers BEFORE pipe() to avoid data loss
|
||||||
|
audioStream.on("data", (chunk: Buffer) => {
|
||||||
|
if (chunk.length < 8) return;
|
||||||
|
segmentManager.rotateIfNeeded(packetFilterForOgg);
|
||||||
|
decoder.rotateIfNeeded();
|
||||||
|
decoder.write(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioStream.on("end", () => {
|
||||||
|
segmentManager.close(packetFilterForOgg);
|
||||||
|
decoder.destroy();
|
||||||
|
_eventBroadcaster?.voiceActiveUser(userId, {
|
||||||
|
username: userMetadata.username,
|
||||||
|
avatar: userMetadata.avatarUrl,
|
||||||
|
speaking: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
audioStream.on("error", (error: Error) => {
|
||||||
|
segmentManager.close(packetFilterForOgg);
|
||||||
|
decoder.destroy();
|
||||||
|
logger.error({ userId, error: error.message }, "Audio stream error");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now pipe for OGG recording (safe — event handlers already attached)
|
||||||
|
const oggPacketStream = audioStream.pipe(packetFilterForOgg);
|
||||||
|
|
||||||
const activeSession = activeSessions.get(channel.guild.id);
|
const activeSession = activeSessions.get(channel.guild.id);
|
||||||
let currentSegment = segmentManager.open(oggPacketStream);
|
let currentSegment = segmentManager.open(oggPacketStream);
|
||||||
currentSegment.out.on("finish", () => {
|
currentSegment.out.on("finish", () => {
|
||||||
@@ -256,30 +288,6 @@ export async function startRecording(
|
|||||||
logger.error({ userId, error: msg }, "File write error");
|
logger.error({ userId, error: msg }, "File write error");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach event handlers directly to the existing audioStream (no double subscription)
|
|
||||||
audioStream.on("data", (chunk: Buffer) => {
|
|
||||||
if (chunk.length < 8) return;
|
|
||||||
segmentManager.rotateIfNeeded(oggPacketStream);
|
|
||||||
decoder.rotateIfNeeded();
|
|
||||||
decoder.write(chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioStream.on("end", () => {
|
|
||||||
segmentManager.close(oggPacketStream);
|
|
||||||
decoder.destroy();
|
|
||||||
_eventBroadcaster?.voiceActiveUser(userId, {
|
|
||||||
username: userMetadata.username,
|
|
||||||
avatar: userMetadata.avatarUrl,
|
|
||||||
speaking: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
audioStream.on("error", (error: Error) => {
|
|
||||||
segmentManager.close(oggPacketStream);
|
|
||||||
decoder.destroy();
|
|
||||||
logger.error({ userId, error: error.message }, "Audio stream error");
|
|
||||||
});
|
|
||||||
|
|
||||||
packetFilterForOgg.on("error", (err) => {
|
packetFilterForOgg.on("error", (err) => {
|
||||||
segmentManager.close(oggPacketStream);
|
segmentManager.close(oggPacketStream);
|
||||||
logger.error({ userId, error: err.message }, "PacketFilter error");
|
logger.error({ userId, error: err.message }, "PacketFilter error");
|
||||||
@@ -292,7 +300,7 @@ export async function startRecording(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle disconnect yang tidak disengaja
|
// Handle unexpected disconnection
|
||||||
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
||||||
if (config.VERBOSE) {
|
if (config.VERBOSE) {
|
||||||
logger.warn("Disconnected from voice channel. Reconnecting...");
|
logger.warn("Disconnected from voice channel. Reconnecting...");
|
||||||
@@ -310,7 +318,7 @@ export async function startRecording(
|
|||||||
config.RECONNECT_TIMEOUT_MS,
|
config.RECONNECT_TIMEOUT_MS,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
// Berhasil reconnect
|
// Reconnected successfully
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Could not reconnect. Destroying connection");
|
logger.error("Could not reconnect. Destroying connection");
|
||||||
connection.destroy();
|
connection.destroy();
|
||||||
@@ -328,7 +336,7 @@ export async function startRecording(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hentikan recording dan disconnect dari voice channel.
|
* Stop recording and disconnect from voice channel.
|
||||||
*/
|
*/
|
||||||
export function stopRecording(guildId: string): void {
|
export function stopRecording(guildId: string): void {
|
||||||
const connection = getVoiceConnection(guildId);
|
const connection = getVoiceConnection(guildId);
|
||||||
|
|||||||
@@ -68,12 +68,11 @@ export async function uploadRecordingSegment(input: {
|
|||||||
await updateVoiceRecordingAsUploaded(id, downloadUrl, Date.now());
|
await updateVoiceRecordingAsUploaded(id, downloadUrl, Date.now());
|
||||||
logger.info({ id, downloadUrl }, "Recording segment uploaded successfully");
|
logger.info({ id, downloadUrl }, "Recording segment uploaded successfully");
|
||||||
|
|
||||||
// 4. Broadcast via WebSocket if broadcaster exists globally
|
// 4. Broadcast via Redis EventBroadcaster (forwarded to WebSocket clients by backend)
|
||||||
const broadcaster = (globalThis as any).moderationBroadcaster;
|
const { _eventBroadcaster } = await import("../recorder.js");
|
||||||
if (broadcaster) {
|
if (_eventBroadcaster) {
|
||||||
const payload = JSON.stringify({
|
_eventBroadcaster
|
||||||
type: "voice_recording_uploaded",
|
.voiceRecordingUploaded({
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
username,
|
username,
|
||||||
@@ -87,26 +86,13 @@ export async function uploadRecordingSegment(input: {
|
|||||||
upload_status: "uploaded",
|
upload_status: "uploaded",
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
uploaded_at: Date.now(),
|
uploaded_at: Date.now(),
|
||||||
},
|
})
|
||||||
timestamp: Date.now(),
|
.catch((err: unknown) => {
|
||||||
});
|
logger.warn(
|
||||||
|
{ err },
|
||||||
broadcaster
|
"Failed to broadcast voice recording upload event",
|
||||||
.getClients()
|
);
|
||||||
.forEach(
|
});
|
||||||
(client: { readyState: number; send: (data: string) => void }) => {
|
|
||||||
if (client.readyState === 1) {
|
|
||||||
try {
|
|
||||||
client.send(payload);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
|
||||||
{ err },
|
|
||||||
"Failed to send recording upload event to client",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ export interface WsEventMap {
|
|||||||
message_updated: { data: unknown };
|
message_updated: { data: unknown };
|
||||||
message_deleted: { data: { id: string } };
|
message_deleted: { data: { id: string } };
|
||||||
message_analyzed: { data: unknown };
|
message_analyzed: { data: unknown };
|
||||||
attachment_uploaded: Record<string, never>;
|
attachment_uploaded: { data: unknown };
|
||||||
user_state: { users: unknown[] };
|
user_state: { users: unknown[] };
|
||||||
ui_state: { state: unknown };
|
ui_state: { state: unknown };
|
||||||
media_state: { state: unknown };
|
media_state: { state: unknown };
|
||||||
voice_recording_uploaded: { data: unknown };
|
voice_recording_uploaded: { data: unknown };
|
||||||
voice_recording_started: { data: unknown };
|
voice_recording_started: { data: unknown };
|
||||||
voice_recording_stopped: { data: unknown };
|
voice_recording_stopped: { data: unknown };
|
||||||
|
voice_pcm_data: { data: unknown };
|
||||||
|
voice_active_user: { data: unknown };
|
||||||
attachment_created: { data: unknown };
|
attachment_created: { data: unknown };
|
||||||
analysis_queue_status: { data: unknown };
|
analysis_queue_status: { data: unknown };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface WsHandlers {
|
|||||||
onMessageUpdated?: (data: unknown) => void;
|
onMessageUpdated?: (data: unknown) => void;
|
||||||
onMessageDeleted?: (data: unknown) => void;
|
onMessageDeleted?: (data: unknown) => void;
|
||||||
onMessageAnalyzed?: (data: unknown) => void;
|
onMessageAnalyzed?: (data: unknown) => void;
|
||||||
onAttachmentUploaded?: () => void;
|
onAttachmentUploaded?: (data: unknown) => void;
|
||||||
onUserState?: (users: unknown[]) => void;
|
onUserState?: (users: unknown[]) => void;
|
||||||
onUiState?: (state: unknown) => void;
|
onUiState?: (state: unknown) => void;
|
||||||
onMediaState?: (state: unknown) => void;
|
onMediaState?: (state: unknown) => void;
|
||||||
@@ -72,7 +72,7 @@ function doConnect(): WebSocket {
|
|||||||
h.onMessageAnalyzed?.(msg.data);
|
h.onMessageAnalyzed?.(msg.data);
|
||||||
break;
|
break;
|
||||||
case "attachment_uploaded":
|
case "attachment_uploaded":
|
||||||
h.onAttachmentUploaded?.();
|
h.onAttachmentUploaded?.(msg.data);
|
||||||
break;
|
break;
|
||||||
case "user_state":
|
case "user_state":
|
||||||
h.onUserState?.((msg.users as unknown[]) || []);
|
h.onUserState?.((msg.users as unknown[]) || []);
|
||||||
@@ -147,7 +147,8 @@ export function useDashboardSocket(handlers: WsHandlers) {
|
|||||||
onMessageUpdated: (d) => handlersRef.current.onMessageUpdated?.(d),
|
onMessageUpdated: (d) => handlersRef.current.onMessageUpdated?.(d),
|
||||||
onMessageDeleted: (d) => handlersRef.current.onMessageDeleted?.(d),
|
onMessageDeleted: (d) => handlersRef.current.onMessageDeleted?.(d),
|
||||||
onMessageAnalyzed: (d) => handlersRef.current.onMessageAnalyzed?.(d),
|
onMessageAnalyzed: (d) => handlersRef.current.onMessageAnalyzed?.(d),
|
||||||
onAttachmentUploaded: () => handlersRef.current.onAttachmentUploaded?.(),
|
onAttachmentUploaded: (d) =>
|
||||||
|
handlersRef.current.onAttachmentUploaded?.(d),
|
||||||
onUserState: (u) => handlersRef.current.onUserState?.(u),
|
onUserState: (u) => handlersRef.current.onUserState?.(u),
|
||||||
onUiState: (s) => handlersRef.current.onUiState?.(s),
|
onUiState: (s) => handlersRef.current.onUiState?.(s),
|
||||||
onMediaState: (s) => handlersRef.current.onMediaState?.(s),
|
onMediaState: (s) => handlersRef.current.onMediaState?.(s),
|
||||||
|
|||||||
Reference in New Issue
Block a user