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:
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user