This commit is contained in:
juanmaldini7 committed 2026-09-28 11:42:07 -03:00
commit ca253f1b16
11 files changed
+2416

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
import {
AttachmentBuilder,
Client,
Events,
GatewayIntentBits,
MessageFlags,
SlashCommandBuilder,
type BaseMessageOptions,
} from 'discord.js';
import { VoiceConnectionStatus, entersState } from '@discordjs/voice';
import { Recording } from './recorder.js';
import { generateNotes } from './gemini.js';
for (const key of ['DISCORD_TOKEN', 'GEMINI_API_KEY']) {
if (!process.env[key]) throw new Error(`Falta la variable de entorno ${key}`);
}
const MAX_MINUTES = 30;
const sessions = new Map<string, Recording>();
const commands = [
new SlashCommandBuilder().setName('start').setDescription('Empieza a grabar tu canal de voz para tomar notas'),
new SlashCommandBuilder().setName('stop').setDescription('Termina la grabación y genera las notas'),
];
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
function fileName(date: Date) {
const p = (n: number) => String(n).padStart(2, '0');
return `notas-${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}.md`;
}
async function finish(guildId: string): Promise<BaseMessageOptions> {
const rec = sessions.get(guildId);
if (!rec) return { content: 'No hay ninguna grabación en curso.' };
sessions.delete(guildId);
try {
const tracks = await rec.stop();
if (tracks.length === 0) return { content: 'No se grabó nada de audio. 🤷' };
const notes = await generateNotes(tracks, rec.startedAt);
const minutes = Math.round((Date.now() - rec.startedAt.getTime()) / 60000);
return {
content: `📝 Notas listas (${minutes} min, ${tracks.map((t) => t.name).join(', ')})`,
files: [new AttachmentBuilder(Buffer.from(notes, 'utf8'), { name: fileName(rec.startedAt) })],
};
} catch (err) {
console.error(err);
return { content: `❌ Error generando las notas: ${(err as Error).message}` };
} finally {
await rec.cleanup();
}
}
// Para cortes automáticos (límite de tiempo, canal vacío, desconexión)
async function autoFinish(guildId: string, reason: string) {
const rec = sessions.get(guildId);
if (!rec) return;
await rec.textChannel.send(`⏹️ ${reason}. Generando notas…`).catch(() => {});
const msg = await finish(guildId);
await rec.textChannel.send(msg).catch(console.error);
}
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(commands.map((cmd) => cmd.toJSON()));
console.log(`ready as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) return;
const { guildId } = interaction;
if (interaction.commandName === 'start') {
const voice = interaction.member.voice.channel;
if (!voice) {
return interaction.reply({ content: 'Entrá a un canal de voz primero.', flags: MessageFlags.Ephemeral });
}
if (sessions.has(guildId)) {
return interaction.reply({ content: 'Ya estoy grabando en este servidor.', flags: MessageFlags.Ephemeral });
}
if (!interaction.channel?.isSendable()) {
return interaction.reply({ content: 'No puedo escribir en este canal.', flags: MessageFlags.Ephemeral });
}
await interaction.deferReply();
try {
const rec = await Recording.start(voice, interaction.channel);
sessions.set(guildId, rec);
rec.timer = setTimeout(() => autoFinish(guildId, `Límite de ${MAX_MINUTES} min alcanzado`), MAX_MINUTES * 60_000);
rec.connection.on(VoiceConnectionStatus.Disconnected, async () => {
try {
await Promise.race([
entersState(rec.connection, VoiceConnectionStatus.Signalling, 5_000),
entersState(rec.connection, VoiceConnectionStatus.Connecting, 5_000),
]);
} catch {
autoFinish(guildId, 'Me desconectaron del canal');
}
});
await interaction.editReply(`🔴 Grabando en ${voice}. Usá \`/stop\` para terminar y recibir las notas.`);
} catch (err) {
console.error(err);
await interaction.editReply('❌ No pude conectarme al canal de voz.');
}
}
if (interaction.commandName === 'stop') {
if (!sessions.has(guildId)) {
return interaction.reply({ content: 'No hay ninguna grabación en curso.', flags: MessageFlags.Ephemeral });
}
await interaction.deferReply();
await interaction.editReply(await finish(guildId));
}
});
// Si todos (menos bots) salen del canal, corta solo
client.on(Events.VoiceStateUpdate, (oldState) => {
const rec = sessions.get(oldState.guild.id);
if (!rec || oldState.channelId !== rec.voiceChannel.id) return;
if (rec.voiceChannel.members.every((m) => m.user.bot)) {
autoFinish(oldState.guild.id, 'Todos salieron del canal');
}
});
client.login(process.env.DISCORD_TOKEN);