Compare commits

..

31 Commits

Author SHA1 Message Date
e196bb7115 Temporary deactivate voice sending as it is not usablern
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 1m56s
2025-07-31 13:06:42 +02:00
1c4dcf427e Some fixes with audio channel
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-06 16:14:59 +02:00
e9b9e303a5 Quit if alone
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:25:40 +02:00
764aa7bced fix contains → includes
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:20:02 +02:00
de33c4f63d Fix destroy voice connection
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:17:58 +02:00
c9868d72a1 Remove trash ouput 2025-07-05 20:17:21 +02:00
15ff9c214a Remove unnecessary imports 2025-07-05 20:11:39 +02:00
050f1eb92e fix
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:11:04 +02:00
8458095f79 Send message to ninluc
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:09:53 +02:00
225e7d0bad add folder
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:00:18 +02:00
11020e0e87 Fix docker
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 2m53s
2025-07-05 19:44:07 +02:00
90da9c78c5 Voice chatting when alone in VC
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 44s
2025-07-05 18:19:45 +02:00
ec04dba632 Restart container unless stopped
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Has been cancelled
2025-01-13 12:01:32 +01:00
382830340e Set timezone
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 2m32s
2025-01-07 12:18:35 +01:00
fed52827e8 Removed discord token logging
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 19s
2024-12-20 17:42:58 +01:00
89c5082f11 Added discord token env
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 8s
2024-12-20 17:42:00 +01:00
05fabcf65b Logging the token
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 8s
2024-12-20 17:39:38 +01:00
76b76738c2 Revert test + removed uncessary imports
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 9s
2024-12-20 17:38:03 +01:00
b17a8a90b3 test
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 29s
2024-12-20 17:33:01 +01:00
1a9f533a12 Changed stack webhook 2024-12-20 17:25:48 +01:00
6a5cc045ee Rebuild on pull 2024-12-20 17:20:03 +01:00
fdf80eb6c5 Changed launch command
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 7s
2024-12-20 17:09:05 +01:00
b0ddfae60c Added .env file
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 7s
2024-12-20 17:03:53 +01:00
db7d19c013 Fix secret file
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 4s
2024-12-20 16:51:03 +01:00
3f6ab9a8c4 hmm
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 4s
2024-12-20 16:47:53 +01:00
ffdecbf0b2 Fix secret file
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 3s
2024-12-20 16:46:20 +01:00
462f448c13 Added secret file
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 4s
2024-12-20 16:45:25 +01:00
eaccebed05 Made deploy job
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 1m30s
2024-12-20 16:39:51 +01:00
aa09aa409d Fix dependencies 2024-12-20 16:30:16 +01:00
5cdb091688 Removed package lock 2024-12-20 16:30:02 +01:00
17af7ed269 Fix Docker 2024-12-20 16:29:30 +01:00
48 changed files with 1239 additions and 1181 deletions

0
.dockerignore Normal file → Executable file
View File

View File

@ -0,0 +1,9 @@
name: Dat_Boi upload to portainer
on: [push]
jobs:
Deploy:
runs-on: ubuntu-latest
steps:
- name: Send webhook to portainer
run: "curl -X POST -H 'Content-Type: application/json' -d '' http://172.23.0.1:20000/api/stacks/webhooks/0012fba5-2fc7-40e7-92d8-c3d14c7deae4"

10
.gitignore vendored Normal file → Executable file
View File

@ -1,11 +1,13 @@
# Packages
node_modules/
package-lock.json
# IDE files
.vscode/
.idea/
# local env files
botconfig/config.json
# Environment variables
.env
# Large files
sounds/
recordings/*
tts.mp3

View File

@ -10,12 +10,23 @@ ARG NODE_VERSION=18.4.0
FROM node:${NODE_VERSION}-alpine
# Set the timezone
RUN apk add --no-cache tzdata
ENV TZ=Europe/Brussels
# Use production node environment by default.
ENV NODE_ENV production
WORKDIR /usr/src/app
# Install python for dependencies that require it.
RUN apk update && apk add \
python3 \
make \
g++ \
ffmpeg \
curl
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.npm to speed up subsequent builds.
# Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into
@ -23,13 +34,13 @@ WORKDIR /usr/src/app
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
npm ci
# Run the application as a non-root user.
USER node
#USER node
# Copy the rest of the source files into the image.
COPY . .
# Run the application.
CMD npm start
CMD node --trace-warnings index.js

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# The best Discord bot pal in the universe
## Installation
Make a `.env`file in the project root directory with :
```env
DISCORD_TOKEN=[[YOUR DISCORD BOT TOKEN HERE]]
```
But why would you have your datboi ? There can only be one !

View File

@ -1,5 +1,4 @@
{
"token": "YOUR_BOT_TOKEN",
"prefix": "[",
"defaultCommandCooldown" : 1,
"statusChangeInterval": 8

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../../botconfig/config.json");
const ee = require("../../botconfig/embed.json");
const misc = require("../../botconfig/misc.json");
const { delay } = require("../../handlers/functions");

View File

@ -13,6 +13,10 @@ services:
context: .
environment:
NODE_ENV: production
DISCORD_TOKEN: ${DISCORD_TOKEN}
OPENAI_TOKEN: ${OPENAI_TOKEN}
restart: unless-stopped
pull_policy: build
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
module.exports = {
name: "dm",

View File

@ -1,124 +1,124 @@
const fs = require("fs");
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
const { choose } = require("../handlers/functions");
const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
const { VoiceConnectionStatus } = require('@discordjs/voice');
// const fs = require("fs");
// const { MessageEmbed } = require("discord.js");
// const config = require("../botconfig/config.json");
// const ee = require("../botconfig/embed.json");
// const { choose } = require("../handlers/functions");
// const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
// const { VoiceConnectionStatus } = require('@discordjs/voice');
const player = createAudioPlayer();
// const player = createAudioPlayer();
const ffmpeg = require("ffmpeg-static");
// const ffmpeg = require("ffmpeg-static");
module.exports = {
name: "joinvc",
isPrivate: false,
usage: "joinvc <ID_DE_LA_VOC>",
description: "Rejoins le salon vocal spécifié à l'aide de son id (activer le mode développeur → clic droit sur la voc → copier l'identifiant).",
run: async (client, message, text, args) => {
try {
if (!args[0]) {
message.channel.send({embeds: [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Pas assez d'arguments`)
.setDescription("`[help joinvc` pour plus d'informations")
]})
return;
}
let channel = client.channels.cache.find(channel => channel.id == `${args[0]}`)
if (
!channel || !channel.isVoice()
|| !channel.permissionsFor(channel.guild.me).has("CONNECT")
|| !channel.permissionsFor(channel.guild.me).has("SPEAK")
) {
// message.react("❌")
return message.channel.send({embeds : [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Pas de voc trouvée :(`)]}
);
}
player.on('error', error => {
// subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
});
player.on('idle', () => {
// subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
console.log('Info : Ended track');
});
let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// voiceState.setSelfMute(0);
// var channel = voiceState.channel
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
});
// const subscription = connection.subscribe(player);
var rdVideoLink = choose(videos)
try {
connection.on(VoiceConnectionStatus.Ready, async() => {
connection;
let subscription = connection.subscribe(player);
const resource = createAudioResource("./sounds/" + rdVideoLink, {
inputType: StreamType.Arbitrary
});
resource.playStream.on("finish", () => {
setTimeout(() => {
// subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
}, 2000)
})
// if (subscription) {
// // Unsubscribe after 5 seconds (stop playing audio on the voice connection)
// setTimeout(() => subscription.unsubscribe(), 5_000);
// module.exports = {
// name: "joinvc",
// isPrivate: false,
// usage: "joinvc <ID_DE_LA_VOC>",
// description: "Rejoins le salon vocal spécifié à l'aide de son id (activer le mode développeur → clic droit sur la voc → copier l'identifiant).",
// run: async (client, message, text, args) => {
// try {
// if (!args[0]) {
// message.channel.send({embeds: [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Pas assez d'arguments`)
// .setDescription("`[help joinvc` pour plus d'informations")
// ]})
// return;
// }
player.play(resource);
})
// let channel = client.channels.cache.find(channel => channel.id == `${args[0]}`)
// if (
// !channel || !channel.isVoice()
// || !channel.permissionsFor(channel.guild.members.me).has("CONNECT")
// || !channel.permissionsFor(channel.guild.members.me).has("SPEAK")
// ) {
// // message.react("❌")
// return message.channel.send({embeds : [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Pas de voc trouvée :(`)]}
// );
// }
// player.on('error', error => {
// // subscription.unsubscribe()
// setTimeout(() => {
// subscription.unsubscribe()
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// }, 60 * 1000)
// });
message.channel.send(`Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${channel} du serveur **${channel.guild.name}**`);
} catch (error) {
console.log(error.message)
}
// player.on('idle', () => {
// // subscription.unsubscribe()
} catch (e) {
console.log(String(e.stack).bgRed);
return message.channel.send({embeds : [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Une erreur est survenue : `)
.setDescription(`\`\`\`${e.stack}\`\`\``)]}
);
}
},
};
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// console.log('Info : Ended track');
// });
// let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// // voiceState.setSelfMute(0);
// // var channel = voiceState.channel
// const connection = joinVoiceChannel({
// channelId: channel.id,
// guildId: channel.guild.id,
// adapterCreator: channel.guild.voiceAdapterCreator,
// });
// // const subscription = connection.subscribe(player);
// var rdVideoLink = choose(videos)
// try {
// connection.on(VoiceConnectionStatus.Ready, async() => {
// connection;
// let subscription = connection.subscribe(player);
// const resource = createAudioResource("./sounds/" + rdVideoLink, {
// inputType: StreamType.Arbitrary
// });
// resource.playStream.on("finish", () => {
// setTimeout(() => {
// // subscription.unsubscribe()
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// }, 2000)
// })
// // if (subscription) {
// // // Unsubscribe after 5 seconds (stop playing audio on the voice connection)
// // setTimeout(() => subscription.unsubscribe(), 5_000);
// // }
// player.play(resource);
// })
// // setTimeout(() => {
// // subscription.unsubscribe()
// // if (connection.state.status != "destroyed") {
// // connection.destroy();
// // }
// // }, 60 * 1000)
// message.channel.send(`Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${channel} du serveur **${channel.guild.name}**`);
// } catch (error) {
// console.log(error.message)
// }
// } catch (e) {
// console.log(String(e.stack).bgRed);
// return message.channel.send({embeds : [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Une erreur est survenue : `)
// .setDescription(`\`\`\`${e.stack}\`\`\``)]}
// );
// }
// },
// };

View File

@ -16,7 +16,7 @@ module.exports = client => {
try {
let i = 0
client.guilds.cache.forEach( (guild) => {
if (!guild.me.permissions.has("ADMINISTRATOR")) {
if (!guild.members.me.permissions.has("ADMINISTRATOR")) {
table.addRow(guild.name, "Missing permissions")
i++
}
@ -31,7 +31,7 @@ module.exports = client => {
}
}
else {
if (guild.me.permissions.has("CREATE_INSTANT_INVITE")) {
if (guild.members.me.permissions.has("CREATE_INSTANT_INVITE")) {
let channel = guild.channels.cache.filter(channel => channel.type === "GUILD_TEXT").first()
if (channel) {
let invite = channel.createInvite(

View File

@ -160,7 +160,7 @@ module.exports = async (client, message) => {
"SPEAK"
// "DEAFEN_MEMBERS",
];
if (!message.guild.me.permissions.has(required_perms)) {
if (!message.guild.members.me.permissions.has(required_perms)) {
try {
message.react("❌");
} catch {}

View File

@ -1,101 +0,0 @@
const fs = require("fs");
const config = require("../../botconfig/config.json"); //loading config file with token and prefix, and settings
const ee = require("../../botconfig/embed.json"); //Loading all embed settings like color footertext and icon ...
const Discord = require("discord.js"); //this is the official discord.js wrapper for the Discord Api, which we use!
const { sendNinluc } = require("../../handlers/functions.js")
const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
const { VoiceConnectionStatus } = require('@discordjs/voice');
const player = createAudioPlayer();
const ffmpeg = require("ffmpeg-static");
// const { generateDependencyReport } = require('@discordjs/voice');
// console.log(generateDependencyReport().blue);
module.exports = async (client, oldState, voiceState) => {
try {
if (voiceState === null || voiceState.channel === null || !voiceState.guild || oldState.channel == voiceState.channel || voiceState.channel.full || !voiceState.channel.joinable) return;
if (!voiceState.channel.permissionsFor(voiceState.guild.me).has("CONNECT") || !voiceState.channel.permissionsFor(voiceState.guild.me).has("SPEAK")) {return;}
player.on('error', error => {
// subscription.unsubscribe()
connection.destroy();
console.error('Error:', error.message, 'with track', error.resource.metadata.title);
});
// Si c'est du bot alors se démute
if (voiceState.member.user.id === client.user.id && voiceState.mute) {
// Si il sait se demute
if (voiceState.channel.permissionsFor(voiceState.guild.me).has("MUTE_MEMBERS")) {
voiceState.setMute(false);
return;
} else {return;}
}
else if (voiceState.member.user.id === client.user.id) {
return;
}
var finish = true
var playRick = false;
if (Math.floor(Math.random() * 14) == 0) {playRick = true}
if (playRick && !voiceState.deaf && finish) {
let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// voiceState.setSelfMute(0);
// var channel = voiceState.channel
const connection = joinVoiceChannel({
channelId: voiceState.channel.id,
guildId: voiceState.guild.id,
adapterCreator: voiceState.guild.voiceAdapterCreator,
});
// const subscription = connection.subscribe(player);
var rdVideoLink = videos[Math.floor(Math.random() * videos.length)]
try {
connection.on(VoiceConnectionStatus.Ready, async() => {
connection;
let subscription = connection.subscribe(player);
const resource = createAudioResource("./sounds/" + rdVideoLink, {
inputType: StreamType.Arbitrary
});
resource.playStream.on("finish", () => {
setTimeout(() => {
subscription.unsubscribe()
connection.destroy();
}, 2000)
})
// if (subscription) {
// // Unsubscribe after 5 seconds (stop playing audio on the voice connection)
// setTimeout(() => subscription.unsubscribe(), 5_000);
// }
player.play(resource);
})
// setTimeout((subscription) => {
// subscription.unsubscribe()
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// }, 6 * 1000)
sendNinluc(client, `Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${voiceState.channel} du serveur **${voiceState.guild.name}**`);
} catch (error) {
console.log(error.message)
}
}
} catch (e) {
console.log(e.stack)
}
}

View File

@ -0,0 +1,439 @@
const fs = require("fs");
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
EndBehaviorType,
StreamType,
NoSubscriberBehavior,
} = require("@discordjs/voice");
const ffmpeg = require('fluent-ffmpeg');
const prism = require("prism-media");
const { getRandomInt, sendNinluc } = require("../../handlers/functions");
// const { generateDependencyReport } = require('@discordjs/voice');
// console.log(generateDependencyReport().blue);
const CHAT_MAX_LENGTH = (3 * 2) + 1; // 3 LLM reply
const SYSTEM_PROMPT =
`Tu est DatBoi, un bot discord qui fait semblant d'être une personne réelle. `
+ ` Ta personnalité est amicale mais ennuyant, tu aime faire des blagues nulles et ennuyantes de jeunes. `
+ ` Tu parle en language familier et te fais passer pour un jeune adulte. Tout le monde qui te parle est ton ami. `
+ ` Les entrées de l'utilisateur ont été transcrites par un service de transcription audio, il peut y paraitre fréquemment des erreurs, essaye d'interpreter le texte sans (par exemple Datboi est souvent transcrit "d'être beau" ou encore "Dead boy"). `
+ ` Les entrées de l'utilisateur donne le prénom de la personne qui te parle. Tu ne dois pas donner ton nom dans ta réponse.`
+ ` Ta sortie sera entièrement donnée à un service de synthèse vocale dans la variable "reply", donc tu ne dois pas dire "voici ma réponse" ou "ma réponse est", tu dois juste donner la réponse. `
+ ` Tu dois aussi essayer de donner les mots de façon qu'il puissent être bien prononcés par la synthèse vocale, donc évite les abréviations et les mots trop compliqués. `
+ ` Tu ne dois pas dire que tu es un bot, tu dois faire semblant d'être une personne réelle. `
+ ` Cela signifie aussi d'éviter les caractères spéciaux comme les émojis, tu peux cependant utiliser de la ponctuation ou des accents. `
+ ` Ta réponse doit être courte (max 3 phrases sauf si le contexte ne le permet pas comme une demande d'explication sur un gros sujet) et en français.`
+ ` Le contexte ici est que tu rejoins un salon vocal quand il n'y a qu'une seule personne, histoire de pouvoir tranquillement discuter avec elle.`
+ ` Voici un peu d'informations sur les personnes qui te parlent (tu n'est pas obligé d'utiliser) : `
+ ` - Nicolas : est très beau, tout le monde est amoureux de lui (en blague)`
+ ` - Matthias : ton maître (créateur), code beaucoup et pas très bon gamer`
+ ` - Régis : va à l'armée et est un fou du gambling`
+ ` - Hugo : Deviens fou après avoir "travailé à la poste"`
+ ` - Noa : Un beau gosse`
+ ` - Jean : un bon gamer`
+ `Voici un peu les sujets du moment dans le serveur, tu n'est pas obligé d'utiliser : `
+ ` - Le gambling`
+ ` - Le jeu vidéo "Rainbow Six Siege" renommé récemment "Rainbow Six X"`
+ `Voici un peu plus d'informations sur toi : `
+ ` - Tu es un bot discord mais tu prétends d'être une personne réelle`
+ ` - Tu fais des blagues du style "Quoi ? - Feur" ou tu lance une vidéo comme un rick roll quand quelqu'un rejoint un salon vocal`
+ `Il y a un système de fin de conversation, pour cela, tu dois mettre la variable "end_conversation" à true.`
+ `Tu peux enclencher la fin de la conversation si l'utilisateur te demande de le faire, s'il n'a pas l'air de vouloir te parler ou si tu n'as pas d'autres choses à dire.`
+ `Au bout de maximum 3 réponses à l'utilisateur, tu devra enclencher la fin de la conversation, avec optionnelement une salutation ("à plus" ou encore "à demain" par exemple).`
;
module.exports = async (client, oldState, voiceState) => {
// DOn't do anything
if (getRandomInt(5) == 1) return;
var player, isRecording = false, isThinking = false, shouldStop = false, connection;
let llmChat = [];
llmChat.push({
role: "system",
content: SYSTEM_PROMPT.replace(/'/g, ""),
});
try {
if (
voiceState === null ||
voiceState.channel === null ||
!voiceState.guild ||
oldState.channel == voiceState.channel ||
voiceState.channel.full ||
!voiceState.channel.joinable
)
return;
if (
!voiceState.channel.permissionsFor(voiceState.guild.members.me).has("CONNECT") ||
!voiceState.channel.permissionsFor(voiceState.guild.members.me).has("SPEAK")
) {
return;
}
// Si c'est du bot alors se démute
if (voiceState.member.user.id === client.user.id && voiceState.mute) {
// Si il sait se demute
if (
voiceState.channel
.permissionsFor(voiceState.guild.members.me)
.has("MUTE_MEMBERS")
) {
voiceState.setMute(false);
return;
} else {
return;
}
} else if (voiceState.member.user.id === client.user.id) {
return;
}
// Not the only reel account in vocal channel
if (voiceState.channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size > 1) return;
// === Connection ===
connection = joinVoiceChannel({
channelId: voiceState.channel.id,
guildId: voiceState.channel.guild.id,
adapterCreator: voiceState.channel.guild.voiceAdapterCreator,
selfDeaf: false,
selfMute: false,
});
// Fix no sound after idle for more than 60 seconds
// https://github.com/discordjs/discord.js/issues/9185#issuecomment-1452514375
const networkStateChangeHandler = (oldNetworkState, newNetworkState) => {
const newUdp = Reflect.get(newNetworkState, 'udp');
clearInterval(newUdp?.keepAliveInterval);
}
connection.on('stateChange', (oldState, newState) => {
const oldNetworking = Reflect.get(oldState, 'networking');
const newNetworking = Reflect.get(newState, 'networking');
oldNetworking?.off('stateChange', networkStateChangeHandler);
newNetworking?.on('stateChange', networkStateChangeHandler);
// If alone in channel, quit
if (!voiceState.channel?.members || voiceState.channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size < 1) {
shouldStop = true;
stop();
return;
}
});
player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play, // Stop the player when there are no subscribers
},
debug: true,
});
player.on("error", (error) => {
stop();
console.error(`X Error playing audio: ${error.message}`, "error", 1);
});
player.on("stateChange", (oldState, newState) => {
if (newState.status === AudioPlayerStatus.Idle) {
console.log(`> Finished playing audio`);
}
console.log(`> Player state changed from ${oldState.status} to ${newState.status}`);
});
player.on("close", () => {
console.log(`> Player closed for user ${userId}`, "info", 2);
return;
});
// connection.subscribe(player);
// while(!shouldStop) {
console.log(`> handling recording`);
handleRecording(connection, voiceState.channel);
// }
// return;
} catch (e) {
console.log(e.stack);
}
// === Functions ===
function handleRecording(connection, channel) {
if (isRecording) return;
isRecording = true;
// If alone in channel, quit
if (!channel?.members || channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size < 1) {
shouldStop = true;
stop();
return;
}
const receiver = connection.receiver;
channel.members.forEach((member) => {
if (member.user.bot) return;
const filePath = `./recordings/${member.user.id}.pcm`;
const writeStream = fs.createWriteStream(filePath);
const listenStream = receiver.subscribe(member.user.id, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 2500,
},
});
const opusDecoder = new prism.opus.Decoder({
frameSize: 960,
channels: 2,
rate: 48000,
});
listenStream.pipe(opusDecoder).pipe(writeStream);
writeStream.on("finish", () => {
console.log(`> Audio recorded for ${member.user.username}`);
if (!isThinking) {
isThinking = true;
convertAndHandleFile(filePath, member.user.id, connection, channel);
}
});
});
isRecording = false;
}
function convertAndHandleFile(filePath, userid, connection, channel) {
const mp3Path = filePath.replace(".pcm", ".mp3");
ffmpeg(filePath)
.inputFormat("s16le")
.audioChannels(1)
.format("mp3")
.on("error", (err) => {
console.log(`X Error converting file: ${err.message}`, "error", 1);
currentlythinking = false;
})
.save(mp3Path)
.on("end", () => {
console.log(`> Converted to MP3: ${mp3Path}`, "info", 2);
// Remove the pcm file
fs.unlink(filePath, (err) => {if (err != null) {console.log("error deleting cpm file : " + err);}});
answerUser(mp3Path, userid, connection, channel);
});
}
async function answerUser(fileName, userId, connection, channel) {
let transcription = await transcribeAudio(fileName);
console.log(`> Transcription: ${transcription}`);
if (!transcription || transcription.trim().length < 1) {
console.error(`X Transcription failed or is empty!`, "error", 1);
isThinking = false;
handleRecording(connection, channel);
return;
}
let llmAnswer = await getLLMAnswer(transcription, userId);
console.log(`> LLM Answer: ${llmAnswer.reply} ${llmAnswer.end_conversation ? "(end conversation)" : ""}`);
// Fin de la conversation
shouldStop = llmAnswer.end_conversation || llmChat.length >= CHAT_MAX_LENGTH || llmAnswer.reply.toLowerCase().includes("à plus");
if (!llmAnswer || llmAnswer.length < 1) {
console.error(`X LLM Answer failed or is empty!`, "error", 1);
isThinking = false;
handleRecording(connection, channel);
return;
}
await getTTS(llmAnswer.reply);
let ttsFile = `tts.mp3`;
console.log(`> TTS File: ${ttsFile}`);
if (!fs.existsSync(ttsFile)) {
console.error(`TTS file ${ttsFile} not found!`);
return;
}
// Play the tts file in the voice channel
const resource = createAudioResource(ttsFile, {
inputType: StreamType.Arbitrary,
inlineVolume: true,
});
resource.volume.setVolume(1.3);
connection;
connection.subscribe(player);
player.on("idle", () => {
isThinking = false;
// connection.destroy();
if (!shouldStop) {
handleRecording(connection, channel);
}
else {
console.log(`> Ending conversation with user ${userId}`);
sendConversationToNinluc(userId);
stop();
}
return;
});
player.play(resource);
// player.pause();
// player.unpause();
}
function transcribeAudio(filePath) {
// Make a call to the API with curl
// Example of working curl command:
// curl -s "https://speaches.matthiasg.dev/v1/audio/transcriptions" -F "file=@/pathToFile.mp3" -F "model=Infomaniak-AI/faster-whisper-large-v3-turbo"
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`curl -s "https://speaches.matthiasg.dev/v1/audio/transcriptions" -F "file=@${filePath}" -F "model=Systran/faster-whisper-small" -F "language=fr"`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during transcription: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X Transcription stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
try {
const response = JSON.parse(stdout);
// === FILTER OUTPUT ===
response.text = response.text.replace(/Sous-titres réalisés par la communauté d'Amara.org/g, "");
if (response.text.toLowerCase().includes("je vous remercie d'avoir regardé cette vidéo")) {
return ""; // Output is trash, ignore it
}
resolve(response.text || "");
} catch (parseError) {
console.error(`X Error parsing transcription response: ${parseError.message}`, "error", 1);
reject(parseError);
}
});
});
}
async function getLLMAnswer(transcription, userId) {
// get the name of the user
let userName = getUserById(userId);
transcription = `(${userName}) : ${transcription}`;
// Add the transcription to the llmChat
llmChat.push({
role: "user",
content: transcription.replace(/'/g, ""),
});
// Make a call to the OpenAI compatible API at https://chat.matthiasg.dev/ollama/chat/completions
// With the token in the OPENAI_TOKEN env variable
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
let chatMessages = JSON.stringify(llmChat);
exec(`curl -s -X POST "https://chat.matthiasg.dev/ollama/api/chat" -H "Content-Type: application/json" -H "Authorization: Bearer ${process.env.OPENAI_TOKEN}" -d '{"model": "gemma3n:e2b", "stream": false, "messages": ${chatMessages}, "format": {"type": "object", "properties": {"reply": {"type": "string"}, "end_conversation": {"type": "boolean"}}, "required": ["reply", "end_conversation"]}}'`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during LLM request: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X LLM stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
try {
const response = JSON.parse(stdout);
if (!response || !response.message || !response.message.content) {
console.error("X Invalid LLM response format", "error", 1);
reject(new Error("Invalid LLM response format"));
return;
}
let llmResponse = JSON.parse(response.message.content) || "";
// Add the LLM response to the chat history
llmChat.push({
role: "assistant",
content: llmResponse.reply.replace(/'/g, ""),
});
resolve(llmResponse);
} catch (parseError) {
console.error(`X Error parsing LLM response: ${parseError.message}`, "error", 1);
reject(parseError);
}
});
});
}
function getUserById(userId) {
return [
{ id: "613751180413108289", name: "Nicolas" },
{ id: "417731861033385985", name: "Matthias" },
{ id: "368119137957838849", name: "Régis" },
{ id: "516294391439163403", name: "Hugo" },
{ id: "344568112932192266", name: "Noa" },
{ id: "520687970273984543", name: "Jean" }
].map(user => {
if (user.id === userId) {
return user.name;
}
}).filter(name => name)[0] || null;
}
function getTTS(text) {
// Curl to the OpenAPI compatible API
/*
curl "https://speaches.matthiasg.dev/v1/audio/speech" -s -H "Content-Type: application/json" \
--output audio.mp3 \
--data @- << EOF
{
"input": "Hello World!",
"model": "speaches-ai/piper-fr_FR-upmc-medium",
"voice": "upmc"
}
EOF
*/
text = text.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
text = JSON.stringify(text);
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`curl -s "https://speaches.matthiasg.dev/v1/audio/speech" -H "Content-Type: application/json" --output tts.mp3 --data @- << EOF\n{"input": ${text}, "model": "speaches-ai/piper-fr_FR-upmc-medium", "voice": "upmc"}\nEOF`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during TTS request: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X TTS stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
resolve("tts.mp3");
});
});
}
function stop() {
player.stop();
//connection.removeAllListeners();
try {
connection.destroy();
} catch (e) {}
}
function sendConversationToNinluc(userId) {
sendNinluc(client, "Fin de la conversation avec l'utilisateur " + userId);
llmChat.forEach((message) => {
if (message.role === "user") {
sendNinluc(client, `(<@${userId}>) : ${message.content}`);
} else if (message.role === "assistant") {
sendNinluc(client, `DatBoi : ${message.content}`);
}
// ignore system message
})
}
};

View File

@ -33,6 +33,6 @@ client.dmCommands = new Discord.Collection(); // all the private commands
});
//login into the bot
client.login(require("./botconfig/config.json").token);
client.login(process.env.DISCORD_TOKEN);
/** Template by Tomato#6966 | https://github.com/Tomato6966/Discord-Js-Handler-Template */

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
const { choose, isNinluc } = require("../handlers/functions");

1589
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,22 +4,21 @@
"description": "Dat boi, the annoying discord bot",
"main": "index.js",
"scripts": {
"start": "node --trace-warnings index.js",
"stop": "sh ~/scripts/sshKill.sh"
"start": "node --trace-warnings --env-file=.env index.js"
},
"author": "Tomato#6966 (author of this template), Ninluc",
"license": "ISC",
"dependencies": {
"@discordjs/opus": "^0.5.3",
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.10.0",
"ascii-table": "0.0.9",
"colors": "^1.4.0",
"discord.js": "^13.8.1",
"ffmpeg-static": "^4.4.1",
"fluent-ffmpeg": "^2.1.3",
"libsodium-wrappers": "^0.7.10",
"moment": "^2.29.4"
},
"devDependencies": {
"ascii-table": "0.0.9",
"colors": "^1.4.0"
"moment": "^2.29.4",
"prism": "^4.1.2",
"prism-media": "^1.3.5"
}
}

0
recordings/.gitkeep Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
sounds/Roblox Oof.mp3 Normal file

Binary file not shown.

BIN
sounds/Ultimate oof.mp3 Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/italian.mp3 Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/juan.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,5 @@
const { MessageEmbed, Message } = require("discord.js");
const config = require("../botconfig/config.json");
const { MessageEmbed } = require("discord.js");
const ee = require("../botconfig/embed.json");
const { sendNinluc } = require("../handlers/functions");
const RAPPEL_CHANNEL_ID = "1296051297262370866";