initibot
This commit is contained in:
commit
ca253f1b16
11 files changed
+2416
No files matched your search
+123
@@ -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);
|
||||
Reference in new issue
Block a user