feat(discord-gateway): implement voice & push improvements

- Voice disconnect broadcast on stopRecording
- Multi-guild voice support (VoiceController Map<guildId>)
- Session finalization + auto-enqueue muxer job
- Recordings API: duration field, channelId/userId filters
- Transmitter Redis connection reuse (shared conn)
- FFmpeg stderr memory cap (4KB limit)
- 10 new Redis event channels + Redis bridge subscriptions
- New DB tables: message_reactions, message_edits
- Webhook notification module
- Gateway metrics / Prometheus endpoint
- Multi-guild message capture (MONITOR_GUILD_IDS array)
- Thread tracking, presence, channel topic, guild member events
- Edit history snapshot on message update
- Muxer audio post-processing worker

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-13 13:51:21 +07:00
co-authored by Claude
parent fd3b5c5ca5
commit 14c0081f01
17 changed files with 260 additions and 90 deletions
@@ -210,24 +210,24 @@ export class DashboardRepository {
[...params, limit + 1],
);
const data = ((rows as Record<string, unknown>[]) || []).slice(0, limit).map((r) => ({
channel_id: String(r.channel_id),
channel_name: r.channel_name as string | null,
guild_id: r.guild_id as string | null,
total_messages: Number(r.total_messages),
flagged_count: Number(r.flagged_count),
last_message_at: r.last_message_at ? Number(r.last_message_at) : null,
culture_summary: r.culture_summary as string | null,
last_analyzed_at: r.last_analyzed_at
? Number(r.last_analyzed_at)
: null,
}));
const data = ((rows as Record<string, unknown>[]) || [])
.slice(0, limit)
.map((r) => ({
channel_id: String(r.channel_id),
channel_name: r.channel_name as string | null,
guild_id: r.guild_id as string | null,
total_messages: Number(r.total_messages),
flagged_count: Number(r.flagged_count),
last_message_at: r.last_message_at ? Number(r.last_message_at) : null,
culture_summary: r.culture_summary as string | null,
last_analyzed_at: r.last_analyzed_at
? Number(r.last_analyzed_at)
: null,
}));
const lastRow = rows[limit - 1] as Record<string, unknown> | undefined;
const nextCursor =
rows.length > limit
? String(lastRow?.total_messages ?? "")
: null;
rows.length > limit ? String(lastRow?.total_messages ?? "") : null;
return { data, nextCursor };
}
@@ -56,9 +56,7 @@ export function createDashboardRouter(): Router {
const search =
typeof req.query.search === "string" ? req.query.search : undefined;
const guildId =
typeof req.query.guild_id === "string"
? req.query.guild_id
: undefined;
typeof req.query.guild_id === "string" ? req.query.guild_id : undefined;
const result = await dashboardService.listChannels({
limit,
@@ -25,7 +25,11 @@ export class DashboardService {
return dashboardRepository.getUserDetail(userId);
}
async listChannels(query: { limit: number; search?: string; guildId?: string }) {
async listChannels(query: {
limit: number;
search?: string;
guildId?: string;
}) {
logger.debug({ query }, "Listing dashboard channels");
return dashboardRepository.listChannels(query);
}
@@ -14,8 +14,13 @@ export function createRecordingsRouter(): Router {
"/recordings",
asyncHandler(async (req: Request, res: Response) => {
const limit = Number(req.query.limit) || 50;
logger.debug({ limit }, "Fetching recordings");
const result = await recordingsService.getRecent(limit);
const channelId = req.query.channelId as string | undefined;
const userId = req.query.userId as string | undefined;
logger.debug({ limit, channelId, userId }, "Fetching recordings");
const result = await recordingsService.getRecent(limit, {
channelId,
userId,
});
res.json(result);
}),
);
@@ -5,17 +5,37 @@ import { getDatabase } from "../../shared/database/index.js";
const logger = createChildLogger("recordings.service");
export class RecordingsService {
async getRecent(limit = 50) {
async getRecent(
limit = 50,
filters?: { channelId?: string; userId?: string },
) {
logger.info({ limit }, "getRecent called");
const db = getDatabase();
logger.debug({ limit }, "Fetching recent voice recordings");
const conditions: string[] = [];
const params: unknown[] = [];
if (filters?.channelId) {
params.push(filters.channelId);
conditions.push(`channel_id = $${params.length}`);
}
if (filters?.userId) {
params.push(filters.userId);
conditions.push(`user_id = $${params.length}`);
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const { rows } = await db.execute(sql`
SELECT
id, user_id, username, avatar_url, guild_id, channel_id,
channel_name, filename, size_bytes, download_url,
upload_status, upload_error, created_at, uploaded_at
upload_status, upload_error, created_at, uploaded_at,
COALESCE(size_bytes, 0) AS duration_bytes
FROM voice_recordings
${sql.raw(whereClause)}
ORDER BY created_at DESC
LIMIT ${limit}
`);
@@ -4,6 +4,7 @@ import {
COMMAND_VOICE_CHANNELS,
COMMAND_VOICE_CONNECT,
COMMAND_VOICE_DISCONNECT,
COMMAND_VOICE_DISCONNECT_GUILD,
CommandReply,
VOICE_STATUS_KEY,
} from "@bete/shared";
@@ -28,11 +29,19 @@ export interface Channel {
type: "voice" | "text";
}
export interface GuildVoiceEntry {
guildId: string;
channelId: string;
channelName: string;
connectedAt: number;
}
export interface VoiceStatus {
connected: boolean;
activeGuildId: string | null;
activeChannelId: string | null;
activeChannelName: string | null;
connections: GuildVoiceEntry[];
}
export const DEFAULT_VOICE_STATUS: VoiceStatus = {
@@ -40,6 +49,7 @@ export const DEFAULT_VOICE_STATUS: VoiceStatus = {
activeGuildId: null,
activeChannelId: null,
activeChannelName: null,
connections: [],
};
/**
@@ -157,3 +167,18 @@ export async function disconnectVoice(): Promise<VoiceStatus> {
"disconnectVoice",
);
}
/**
* Disconnect from a specific guild's voice channel.
*/
export async function disconnectVoiceGuild(
guildId: string,
): Promise<VoiceStatus> {
logger.info({ guildId }, "disconnectVoiceGuild called");
return withFallback(
() =>
publishCommand<VoiceStatus>(COMMAND_VOICE_DISCONNECT_GUILD, { guildId }),
() => readVoiceStatusFallback(),
"disconnectVoiceGuild",
);
}
+21
View File
@@ -2,10 +2,19 @@ import {
DISCORD_ANALYSIS_QUEUE_STATUS,
DISCORD_ATTACHMENT_CREATED,
DISCORD_ATTACHMENT_UPLOADED,
DISCORD_CHANNEL_TOPIC_UPDATED,
DISCORD_GUILD_MEMBER_ADDED,
DISCORD_GUILD_MEMBER_REMOVED,
DISCORD_MESSAGE_ANALYZED,
DISCORD_MESSAGE_CREATED,
DISCORD_MESSAGE_DELETED,
DISCORD_MESSAGE_UPDATED,
DISCORD_PRESENCE_UPDATED,
DISCORD_REACTION_ADDED,
DISCORD_REACTION_REMOVED,
DISCORD_THREAD_CREATED,
DISCORD_THREAD_DELETED,
DISCORD_THREAD_UPDATED,
DISCORD_VOICE_ACTIVE_USER,
DISCORD_VOICE_PCM,
DISCORD_VOICE_STARTED,
@@ -40,6 +49,18 @@ const SUBSCRIPTIONS: ChannelMapping[] = [
},
{ channel: DISCORD_VOICE_ACTIVE_USER, eventType: "voice_active_user" },
{ channel: DISCORD_VOICE_PCM, eventType: "voice_pcm_data" },
{ channel: DISCORD_REACTION_ADDED, eventType: "reaction_added" },
{ channel: DISCORD_REACTION_REMOVED, eventType: "reaction_removed" },
{ channel: DISCORD_THREAD_CREATED, eventType: "thread_created" },
{ channel: DISCORD_THREAD_DELETED, eventType: "thread_deleted" },
{ channel: DISCORD_THREAD_UPDATED, eventType: "thread_updated" },
{
channel: DISCORD_CHANNEL_TOPIC_UPDATED,
eventType: "channel_topic_updated",
},
{ channel: DISCORD_PRESENCE_UPDATED, eventType: "presence_updated" },
{ channel: DISCORD_GUILD_MEMBER_ADDED, eventType: "guild_member_added" },
{ channel: DISCORD_GUILD_MEMBER_REMOVED, eventType: "guild_member_removed" },
];
let subscriber: Redis | null = null;