Réécriture complète 4.0
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m16s

This commit is contained in:
2025-06-09 16:29:12 +02:00
parent f2c6388da6
commit ddd617317c
133 changed files with 8092 additions and 4332 deletions

View File

@@ -1,12 +0,0 @@
.git
.vscode
node_modules
public/cracks/*
.dockerignore
.env
.gitignore
.ncurc.json
Dockerfile
eslint.config.mjs
README.md
tsconfig.json

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Configuration du bot Discord
DISCORD_APP_ID=votre_app_id_discord
DISCORD_TOKEN=votre_token_discord
DISCORD_SPT_GUILD_ID=id_guild_salonpostam
# Configuration Twitch
TWITCH_APP_ID=votre_app_id_twitch
TWITCH_APP_SECRET=votre_secret_twitch
# Configuration Twurple (pour les webhooks)
TWURPLE_HOSTNAME=localhost
TWURPLE_PORT=3000
TWURPLE_SECRET=VeryUnsecureSecretPleaseChangeMe
# Configuration Ngrok (pour le développement)
NGROK_AUTHTOKEN=votre_token_ngrok
# Configuration MongoDB
MONGOOSE_USER=utilisateur_mongodb
MONGOOSE_PASSWORD=mot_de_passe_mongodb
MONGOOSE_HOST=localhost:27017
MONGOOSE_DATABASE=nom_base_de_donnees
# Configuration d'environnement
NODE_ENV=development
# Configuration des locales
DEFAULT_LOCALE=fr
CONSOLE_LOCALE=en

View File

@@ -26,6 +26,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
run: npm run build --if-present
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -36,20 +46,14 @@ jobs:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }}
tags: |
# Tag avec le nom de la branche
type=ref,event=branch
# Tag avec le nom du tag Git
type=ref,event=tag
# Tag avec le SHA du commit
type=sha,prefix={{branch}}-
# Tag latest pour la branche master
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.title=${{ env.IMAGE_NAME }}
org.opencontainers.image.description=Bot Discord

22
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,22 @@
# Instructions pour GitHub Copilot
## Contexte du projet
Ce projet est un bot appelé "bot_Tamiseur" développé en TypeScript.
Il s'agit d'un bot Discord pour des tâches de musique ou de notifications Twitch.
Des fonctionnalités spécifiques telles que amp, freebox... ont été implémentées par moi-même pour intérager avec mon infrastructure.
## Conventions de code
- Utiliser des noms de variables en anglais
- Suivre les conventions de nommage du langage utilisé
- Commenter le code en français
- Utiliser des messages de commit en français
- Priviléger l'écriture sur une seule ligne pour les fonctions simples si la longueur n'est pas excessive
## Préférences de développement
- Suivre les pratiques de développement du projet, telles que du code compressé en lisibilité et optimisé par exemple
- Comprendre les fonctionnalités du bot et les interactions avec Discord
- Éviter le spaghetti code et ne pas ajouter de code inutile
- Ne pas écrire énormément de code pour une seule demande, favoriser une approche simple et efficace
- Si demandé, proposer une solution plus développée
- Toujours repasser sur ce que tu as modifié pour retirer les erreurs potentielles
- Ne pas proposer une solution juste pour avoir une solution

5
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.env
dist/
node_modules/
public/cracks/*
.env*
.ncurc.json
public/cracks/

18
.vscode/launch.json vendored
View File

@@ -1,18 +0,0 @@
{
// Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
// Pointez pour afficher la description des attributs existants.
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Nodemon",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "nodemon",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"restart": true
}
]
}

View File

@@ -1,3 +0,0 @@
{
"docker.commands.build": "docker build --rm -f \"${dockerfile}\" -t bot_tamiseur:latest -t localhost:5000/bot_tamiseur:latest \"${context}\" && docker push localhost:5000/bot_tamiseur:latest"
}

View File

@@ -1,19 +0,0 @@
FROM node:lts-alpine
RUN apk add --no-cache ffmpeg python3 make g++
RUN npm install -g ts-node
RUN mkdir -p /usr/src/app/node_modules
WORKDIR /usr/src/app
COPY package*.json ./
RUN chown -R node:node /usr/src/app
USER node
#ENV NODE_ENV=production
#RUN npm install --production --verbose
RUN npm install --verbose
COPY --chown=node:node . .
CMD ["npm", "start"]
#CMD ["npm", "run", "prod"]

View File

@@ -2,7 +2,6 @@
# Build new application
# =====================
.PHONY: tag-build
tag-build: ## DEV : Build a prod version with a timestamped tag
@export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \

128
README.md
View File

@@ -1,3 +1,129 @@
# Discord
# Bot Tamiseur
Bot Discord multifonction développé en TypeScript pour la musique, les notifications Twitch et l'intégration avec diverses infrastructures.
## Fonctionnalités
### 🎵 Lecteur de musique
- Lecture de musique depuis diverses sources (YouTube, Spotify, etc.)
- Gestion de file d'attente
- Mode disco avec sauvegarde automatique
- Contrôles avancés (boucle, mélange, volume)
### 📺 Notifications Twitch
- Notifications automatiques de streams en direct
- Intégration EventSub avec Twurple
- Gestion multi-streamers
### 🔧 Intégrations infrastructure
- **AMP**: Gestion des instances de serveurs de jeu
- **Freebox**: Intégration avec l'API Freebox
- **Crack**: Fonctionnalités spécifiques pour SalonPostam
## Installation
### Prérequis
- Node.js 22+
- MongoDB
- Compte Discord Developer
- Compte Twitch Developer (optionnel)
### Configuration
1. Cloner le repository :
```bash
git clone <url_repository>
cd bot_Tamiseur
```
2. Installer les dépendances :
```bash
npm install
```
3. Configurer les variables d'environnement :
```bash
cp .env.example .env
# Éditer le fichier .env avec vos valeurs
```
### Variables d'environnement requises
Voir le fichier `.env.example` pour la liste complète des variables.
**Variables essentielles :**
- `DISCORD_APP_ID` et `DISCORD_TOKEN` : Configuration Discord
- `MONGOOSE_*` : Configuration base de données MongoDB
**Variables optionnelles :**
- `TWITCH_*` : Pour les notifications Twitch
- `NGROK_AUTHTOKEN` : Pour le développement avec webhooks Twitch
- `DEFAULT_LOCALE` : Locale par défaut (défaut: `fr`)
- `CONSOLE_LOCALE` : Locale pour les logs console (défaut: `en`)
## Utilisation
### Développement
```bash
npm run dev
```
### Production
```bash
npm run build
npm run start:prod
```
### Scripts disponibles
- `npm run dev` : Démarrage en mode développement avec watch
- `npm run build` : Build de production
- `npm run start:prod` : Démarrage en production
- `npm run lint` : Vérification du code
- `npm run lint:fix` : Correction automatique des erreurs de linting
## Architecture
Le projet suit une architecture modulaire TypeScript :
- `/src/commands/` : Commandes slash Discord organisées par catégories
- `/src/events/` : Gestionnaires d'événements (Discord, MongoDB, Player)
- `/src/buttons/` et `/src/selectmenus/` : Interactions utilisateur
- `/src/utils/` : Modules utilitaires (player, twitch, amp, freebox)
- `/src/types/` : Définitions TypeScript
- `/src/schemas/` : Schémas MongoDB
## Déploiement
Le projet inclut des configurations Docker et Helm pour le déploiement en Kubernetes :
```bash
# Build et tag pour déploiement
make tag-build
make tag-deploy
```
## Développement
Le code suit les conventions définies dans `.github/copilot-instructions.md` :
- Variables en anglais, commentaires en français
- Code optimisé et lisible
- Architecture modulaire stricte
### Internationalisation
Le bot supporte plusieurs langues via le système i18n :
**Fonctions de traduction :**
- `t(locale, key, params)` : Traduction basée sur la locale utilisateur
- `fr(key, params)` : Traduction en français
- `en(key, params)` : Traduction en anglais
**Configuration des locales :**
- `DEFAULT_LOCALE` : Locale par défaut pour les utilisateurs
- `CONSOLE_LOCALE` : Locale pour les messages de logs console
**Fichiers de traduction :**
- `/src/locales/fr.json` : Traductions françaises
- `/src/locales/en.json` : Traductions anglaises

24
build/node.dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Starting from node
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
RUN apk add --no-cache python3 make g++
# Copy package files and install only production dependencies
COPY package.json package-lock.json* .
RUN npm ci --only=production --ignore-scripts && \
npm install bufferutil zlib-sync
# Copy the builded files and the charts
COPY ./dist/* .
# Set the permissions
RUN chown -R node:node /app
USER node
# Start the application
CMD ["npm", "start"]

View File

@@ -3,7 +3,7 @@ deployment:
strategy: RollingUpdate
image:
repository: "rgy.angels-dev.fr/prod/bot_tamiseur"
tag: "3.0.4"
tag: "4.0.0"
pullPolicy: IfNotPresent
env:
NODE_ENV: "production"

103
docs/FREEBOX_LCD.md Normal file
View File

@@ -0,0 +1,103 @@
# Système de Contrôle des LEDs Freebox
Ce module permet au bot de contrôler automatiquement les LEDs de l'écran LCD de la Freebox avec des timers programmables.
## Fonctionnalités
### Contrôle Manuel des LEDs
- Allumer/éteindre les LEDs instantanément via commande Discord
- Récupérer la configuration actuelle de l'écran LCD
### Timer Automatique
- Programmation d'extinction automatique la nuit
- Programmation d'allumage automatique le matin
- Gestion par bot unique par serveur (évite les conflits)
- Persistance des paramètres en base de données
## Configuration Prérequise
1. **Module Freebox activé** : `/database edit guildFbx.enabled true`
2. **Hôte configuré** : `/database edit guildFbx.host <ip_freebox>`
3. **Version API configurée** : `/database edit guildFbx.version <version>`
4. **Authentification** : `/freebox init` (suivre le processus d'autorisation)
## Commandes Disponibles
### Récupération de configuration
```
/freebox get lcd
```
Récupère et affiche la configuration actuelle de l'écran LCD.
### Contrôle manuel des LEDs
```
/freebox lcd leds enabled:true # Allumer les LEDs
/freebox lcd leds enabled:false # Éteindre les LEDs
```
### Gestion du timer
```
# Activer le timer avec horaires
/freebox lcd timer action:enable morning_time:08:00 night_time:22:30
# Vérifier le statut du timer
/freebox lcd timer action:status
# Désactiver le timer
/freebox lcd timer action:disable
```
## Fonctionnement du Timer
### Programmation
- Le timer est programmé automatiquement au démarrage du bot
- Seul le bot configuré comme "gestionnaire" peut contrôler les LEDs d'un serveur
- Les horaires sont vérifiés et formatés (HH:MM, 24h)
### Exécution
- **Matin** : Les LEDs s'allument à l'heure programmée
- **Soir** : Les LEDs s'éteignent à l'heure programmée
- **Reprogrammation** : Le cycle se répète automatiquement chaque jour
### Logs
Les opérations sont loggées dans la console :
- Programmation des timers
- Exécution des commandes d'allumage/extinction
- Erreurs de connexion ou d'authentification
## Gestion Multi-Bot
### Système de Verrouillage
- Un seul bot peut gérer les LEDs par serveur Discord
- L'ID du bot gestionnaire est stocké en base de données
- Les autres bots reçoivent un message d'erreur s'ils tentent d'utiliser les commandes
### Changement de Bot Gestionnaire
Pour changer de bot gestionnaire :
1. Désactiver le timer sur le bot actuel : `/freebox lcd timer action:disable`
2. Activer le timer sur le nouveau bot : `/freebox lcd timer action:enable`
## Dépannage
### Erreurs Communes
- **"Module Freebox désactivé"** : Activer avec `/database edit guildFbx.enabled true`
- **"Hôte non configuré"** : Définir avec `/database edit guildFbx.host <ip>`
- **"Token d'app manquant"** : Refaire l'initialisation avec `/freebox init`
- **"Géré par un autre bot"** : Désactiver sur l'autre bot d'abord
### Vérification de Configuration
1. Vérifier que la Freebox est accessible sur le réseau
2. S'assurer que l'application est autorisée dans l'interface Freebox
3. Vérifier les logs de la console pour les erreurs détaillées
## API Freebox Utilisée
- `GET /api/v8/lcd/config/` : Récupération de la configuration LCD
- `PUT /api/v8/lcd/config/` : Modification de la configuration LCD
- Propriété `led_strip_enabled` : Contrôle de l'état des LEDs
## Sécurité
- Les tokens d'authentification sont gérés automatiquement
- Les sessions sont créées à la demande
- Les erreurs d'authentification sont loggées mais les tokens ne sont pas exposés

View File

@@ -1,23 +1,22 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin"
import tsParser from "@typescript-eslint/parser"
import { FlatCompat } from "@eslint/eslintrc"
import { fileURLToPath } from "node:url"
import path from "node:path"
import js from "@eslint/js"
import eslint from "@eslint/js"
import tseslint from "typescript-eslint"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
})
export default [
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
plugins: { "@typescript-eslint": typescriptEslint },
languageOptions: { parser: tsParser },
rules: { "prefer-const": "off" }
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{ ignores: ["dist/**", "eslint.config.mjs", "tsup.config.ts"] },
{
rules: { "@typescript-eslint/restrict-template-expressions": "off" },
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"]
}
]
)

3664
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,52 @@
{
"name": "bot_tamiseur",
"description": "Listen to music and use fun commands with your friends!",
"version": "3.0.4",
"version": "4.0.0",
"author": {
"name": "Zachary Guénot"
},
"main": "src/index.ts",
"scripts": {
"format": "prettier --write .",
"start": "npx tsx src/index.ts",
"dev": "nodemon -e ts src/index.ts",
"build": "tsc",
"lint": "eslint src/**/*.ts",
"prod": "node dist/index.js"
"start": "node index.js",
"start:prod": "NODE_ENV=production node dist/index.js",
"start:dev": "NODE_ENV=development tsx src/index.ts",
"dev": "NODE_ENV=development tsx watch src/index.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"build": "tsup",
"updateall": "ncu -u && npm i"
},
"//": [
"Garder chalk à la version 4.1.2 pour éviter un bug ESM avec la version >=5.0.0"
"Garder parse-torrent à la version 9.1.5 pour éviter un bug exports avec la version >=10.0.0"
],
"dependencies": {
"@discord-player/equalizer": "^7.0.0",
"@discord-player/extractor": "^7.0.0",
"@discord-player/extractor": "^7.1.0",
"@discordjs/voice": "^0.18.0",
"@evan/opus": "^1.0.3",
"axios": "^1.7.9",
"@twurple/api": "^7.3.0",
"@twurple/auth": "^7.3.0",
"@twurple/eventsub-http": "^7.3.0",
"@twurple/eventsub-ngrok": "^7.3.0",
"axios": "^1.9.0",
"bufferutil": "^4.0.9",
"chalk": "^4.1.2",
"discord-player": "^7.0.0",
"discord-player-youtubei": "^1.3.7",
"discord.js": "^14.17.2",
"dotenv": "^16.4.7",
"chalk": "^5.4.1",
"discord-player": "^7.1.0",
"discord-player-youtubei": "^1.4.6",
"discord.js": "^14.19.3",
"iconv-lite": "^0.6.3",
"jsdom": "^25.0.1",
"libsodium-wrappers": "^0.7.15",
"mediaplex": "^1.0.0",
"mongoose": "^8.9.3",
"mongoose": "^8.15.1",
"parse-torrent": "^9.1.5",
"require-all": "^3.0.0",
"rss-parser": "^3.13.0",
"utf-8-validate": "^6.0.5",
"websocket": "^1.0.35"
"zlib-sync": "^0.1.10"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@swc/core": "^1.10.4",
"@types/node": "^22.10.5",
"@eslint/js": "^9.28.0",
"@types/node": "^22.15.30",
"@types/parse-torrent": "^5.8.7",
"@types/websocket": "^1.0.10",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"eslint": "^9.17.0",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"tsx": "^4.19.2"
"dotenv": "^16.5.0",
"eslint": "^9.28.0",
"tsup": "^8.5.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1"
}
}

View File

@@ -0,0 +1,11 @@
import * as lcd_status from "./lcd_status"
import * as refresh_status from "./refresh_status"
import * as test_connection from "./test_connection"
import type { Button } from "@/types"
export default [
lcd_status,
refresh_status,
test_connection
] as Button[]

View File

@@ -0,0 +1,88 @@
import { EmbedBuilder, MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import crypto from "crypto"
import * as Freebox from "@/utils/freebox"
import type { APIResponseData, APIResponseDataError, GetChallenge, LcdConfig, OpenSession } from "@/types/freebox"
import type { GuildFbx } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "freebox_lcd_status"
export async function execute(interaction: ButtonInteraction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildFbx") as GuildFbx
if (!dbData.enabled || !dbData.host || !dbData.appToken) {
return interaction.followUp({ content: t(interaction.locale, "freebox.general.incomplete_configuration"), flags: MessageFlags.Ephemeral })
}
try {
// Connexion à l'API Freebox
const challengeData = await Freebox.Login.Challenge(dbData.host) as APIResponseData<GetChallenge>
if (!challengeData.success) return await Freebox.handleError(challengeData as APIResponseDataError, interaction, false)
const password = crypto.createHmac("sha1", dbData.appToken).update(challengeData.result.challenge).digest("hex")
const sessionData = await Freebox.Login.Session(dbData.host, password) as APIResponseData<OpenSession>
if (!sessionData.success) return await Freebox.handleError(sessionData as APIResponseDataError, interaction, false)
const sessionToken = sessionData.result.session_token
if (!sessionToken) return await interaction.followUp({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral })
// Récupération de la configuration LCD
const lcdData = await Freebox.Get.LcdConfig(dbData.host, sessionToken) as APIResponseData<LcdConfig>
if (!lcdData.success) return await Freebox.handleError(lcdData as APIResponseDataError, interaction, false)
const lcdConfig = lcdData.result
// Création de l'embed avec les informations LCD
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "freebox.lcd.config_title"))
.setColor(lcdConfig.led_strip_enabled ? 0x00ff00 : 0xff0000)
.addFields(
{
name: t(interaction.locale, "freebox.lcd.led_strip"),
value: lcdConfig.led_strip_enabled ? t(interaction.locale, "freebox.lcd.led_strip_on") : t(interaction.locale, "freebox.lcd.led_strip_off"),
inline: true
},
{
name: t(interaction.locale, "freebox.lcd.brightness"),
value: `${lcdConfig.brightness}%`,
inline: true
},
{
name: t(interaction.locale, "freebox.lcd.orientation"),
value: lcdConfig.orientation === 0 ? t(interaction.locale, "freebox.lcd.orientation_portrait") : t(interaction.locale, "freebox.lcd.orientation_landscape"),
inline: true
}
)
// Informations du timer si configuré
if (dbData.lcd) {
const timerStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.status.timer_enabled") : t(interaction.locale, "freebox.status.timer_disabled")
const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager")
const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
embed.addFields({
name: t(interaction.locale, "freebox.lcd.timer_auto"),
value: [
t(interaction.locale, "freebox.timer.status_field", { status: timerStatus }),
t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }),
t(interaction.locale, "freebox.timer.morning", { time: morningTime }),
t(interaction.locale, "freebox.timer.night", { time: nightTime })
].join('\n'),
inline: false
})
}
embed.setTimestamp()
return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral })
} catch (error) {
console.error("Erreur lors de la récupération de l'état LCD:", error)
return interaction.followUp({ content: t(interaction.locale, "freebox.lcd.unexpected_error"), flags: MessageFlags.Ephemeral })
}
}

View File

@@ -0,0 +1,76 @@
import { EmbedBuilder, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import type { GuildFbx } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "freebox_refresh_status"
export async function execute(interaction: ButtonInteraction) {
await interaction.deferUpdate()
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildFbx") as GuildFbx
// Reconstruire l'embed de statut actualisé
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "freebox.status.title"))
.setColor(dbData.enabled ? 0x00ff00 : 0xff0000)
.addFields(
{
name: t(interaction.locale, "freebox.status.config_section"),
value: [
t(interaction.locale, "freebox.status.module_field", { status: dbData.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }),
t(interaction.locale, "freebox.status.host_field", { value: dbData.host ? `\`${dbData.host}\`` : t(interaction.locale, "freebox.status.host_not_configured") }),
t(interaction.locale, "freebox.status.token_field", { status: dbData.appToken ? t(interaction.locale, "freebox.status.token_configured") : t(interaction.locale, "freebox.status.token_not_configured") })
].join('\n'),
inline: false
}
)
// Informations LCD si disponibles
if (dbData.lcd) {
const lcdStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled")
const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager")
const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
embed.addFields({
name: t(interaction.locale, "freebox.status.timer_section"),
value: [
t(interaction.locale, "freebox.timer.status_field", { status: lcdStatus }),
t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }),
t(interaction.locale, "freebox.timer.morning", { time: morningTime }),
t(interaction.locale, "freebox.timer.night", { time: nightTime })
].join('\n'),
inline: false
})
}
embed.setTimestamp()
// Reconstruire les boutons
const buttons = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId("freebox_test_connection")
.setLabel(t(interaction.locale, "freebox.buttons.test_connection"))
.setEmoji("🔌")
.setStyle(ButtonStyle.Primary)
.setDisabled(!dbData.appToken),
new ButtonBuilder()
.setCustomId("freebox_lcd_status")
.setLabel(t(interaction.locale, "freebox.buttons.lcd_status"))
.setEmoji("💡")
.setStyle(ButtonStyle.Secondary)
.setDisabled(!dbData.appToken),
new ButtonBuilder()
.setCustomId("freebox_refresh_status")
.setLabel(t(interaction.locale, "freebox.buttons.refresh_status"))
.setEmoji("🔄")
.setStyle(ButtonStyle.Success)
)
return interaction.editReply({ embeds: [embed], components: [buttons] })
}

View File

@@ -0,0 +1,71 @@
import { EmbedBuilder, MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import crypto from "crypto"
import * as Freebox from "@/utils/freebox"
import type { APIResponseData, APIResponseDataError, APIResponseDataVersion, ConnectionStatus, GetChallenge, OpenSession } from "@/types/freebox"
import type { GuildFbx } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "freebox_test_connection"
export async function execute(interaction: ButtonInteraction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildFbx") as GuildFbx
if (!dbData.enabled || !dbData.host || !dbData.appToken) {
return interaction.followUp({ content: t(interaction.locale, "freebox.general.incomplete_configuration"), flags: MessageFlags.Ephemeral })
}
try {
// Test de la version API
const versionData = await Freebox.Core.Version(dbData.host) as APIResponseDataVersion
// Test d'authentification
const challengeData = await Freebox.Login.Challenge(dbData.host) as APIResponseData<GetChallenge>
if (!challengeData.success) return await Freebox.handleError(challengeData as APIResponseDataError, interaction, false)
const password = crypto.createHmac("sha1", dbData.appToken).update(challengeData.result.challenge).digest("hex")
const sessionData = await Freebox.Login.Session(dbData.host, password) as APIResponseData<OpenSession>
if (!sessionData.success) return await Freebox.handleError(sessionData as APIResponseDataError, interaction, false)
const sessionToken = sessionData.result.session_token
if (!sessionToken) return await interaction.followUp({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral })
// Test de la connexion
const connectionData = await Freebox.Get.Connection(dbData.host, sessionToken) as APIResponseData<ConnectionStatus>
if (!connectionData.success) return await Freebox.handleError(connectionData as APIResponseDataError, interaction, false)
// Création de l'embed de succès
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "freebox.test.connection_success_title"))
.setColor(0x00ff00)
.addFields(
{
name: t(interaction.locale, "freebox.test.api_field"),
value: t(interaction.locale, "freebox.test.api_version", { version: versionData.api_version }),
inline: true
},
{
name: t(interaction.locale, "freebox.test.auth_field"),
value: t(interaction.locale, "freebox.test.token_valid"),
inline: true
},
{
name: t(interaction.locale, "freebox.test.connection_field"),
value: connectionData.result.state === "up" ?
t(interaction.locale, "freebox.test.connection_active") :
t(interaction.locale, "freebox.test.connection_inactive"),
inline: true
}
)
.setTimestamp()
return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral })
} catch (error) {
console.error("Erreur lors du test de connexion Freebox:", error)
return interaction.followUp({ content: t(interaction.locale, "freebox.test.connection_error"), flags: MessageFlags.Ephemeral })
}
}

26
src/buttons/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import freebox from "./freebox"
import player from "./player"
import twitch from "./twitch"
import type { Button, ButtonFolder } from "@/types"
export const buttonFolders = [
{
name: "freebox",
commands: freebox
},
{
name: "player",
commands: player
},
{
name: "twitch",
commands: twitch
}
] as ButtonFolder[]
export default [
...freebox,
...player,
...twitch
] as Button[]

View File

@@ -1,16 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'loop',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
let loop = queue.repeatMode === 0 ? 1 : queue.repeatMode === 1 ? 2 : queue.repeatMode === 2 ? 3 : 0
await queue.setRepeatMode(loop)
await interaction.followUp({ content:`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`, ephemeral: true })
}
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'pause',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
await queue.node.setPaused(!queue.node.isPaused())
return interaction.followUp({ content: 'Musique mise en pause !', ephemeral: true })
}
}

View File

@@ -0,0 +1,15 @@
import { MessageFlags, ChannelType, ActionRowBuilder, ChannelSelectMenuBuilder } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const id = "player_disco_channel"
export async function execute(interaction: ButtonInteraction) {
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId("player_disco_channel")
.setPlaceholder(t(interaction.locale, "selectmenus.placeholders.select_channel_disco"))
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
const row = new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(channelSelect)
return interaction.reply({ content: t(interaction.locale, "player.disco.select_channel"), components: [row], flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,24 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { generateDiscoEmbed } from "@/utils/player"
import type { Disco } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "player_disco_disable"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildPlayer.disco") as Disco
dbData.enabled = false
guildProfile.set("guildPlayer.disco", dbData)
guildProfile.markModified("guildPlayer.disco")
await guildProfile.save().catch(console.error)
// Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour
const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.update({ embeds: [embed], components: components })
}

View File

@@ -0,0 +1,31 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { generateDiscoEmbed } from "@/utils/player"
import type { Disco } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "player_disco_enable"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildPlayer.disco") as Disco
// Vérifier qu'un canal est configuré avant d'activer
if (!dbData.channelId) return interaction.reply({
content: t(interaction.locale, "player.disco.configure_channel_first"),
flags: MessageFlags.Ephemeral
})
dbData.enabled = true
guildProfile.set("guildPlayer.disco", dbData)
guildProfile.markModified("guildPlayer.disco")
await guildProfile.save().catch(console.error)
// Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour
const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.update({ embeds: [embed], components: components })
}

View File

@@ -0,0 +1,29 @@
import * as disco_disable from "./disco_disable"
import * as disco_enable from "./disco_enable"
import * as disco_channel from "./disco_channel"
import * as loop from "./loop"
import * as pause from "./pause"
import * as previous from "./previous"
import * as resume from "./resume"
import * as shuffle from "./shuffle"
import * as skip from "./skip"
import * as stop from "./stop"
import * as volume_down from "./volume_down"
import * as volume_up from "./volume_up"
import type { Button } from "@/types"
export default [
disco_channel,
disco_disable,
disco_enable,
loop,
pause,
previous,
resume,
shuffle,
skip,
stop,
volume_down,
volume_up
] as Button[]

View File

@@ -0,0 +1,22 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_loop"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
const loop = queue.repeatMode === 0 ? 1 : queue.repeatMode === 1 ? 2 : queue.repeatMode === 2 ? 3 : 0
queue.setRepeatMode(loop)
const loopModes = {
0: t(interaction.locale, "player.loop_off"),
1: t(interaction.locale, "player.loop_track"),
2: t(interaction.locale, "player.loop_queue"),
3: t(interaction.locale, "player.loop_autoplay")
}
return interaction.followUp({ content: loopModes[loop], flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,13 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_pause"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
queue.node.setPaused(!queue.node.isPaused())
return interaction.followUp({ content: t(interaction.locale, "player.paused"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,13 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useHistory } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_previous"
export async function execute(interaction: ButtonInteraction) {
const history = useHistory(interaction.guild?.id ?? "")
if (!history) return
await history.previous()
return interaction.followUp({ content: t(interaction.locale, "player.previous_played"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,13 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_resume"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
queue.node.setPaused(!queue.node.isPaused())
return interaction.followUp({ content: t(interaction.locale, "player.resumed"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,13 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_shuffle"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
queue.tracks.shuffle()
return interaction.followUp({ content: t(interaction.locale, "player.shuffled"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,13 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_skip"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
queue.node.skip()
return interaction.followUp({ content: t(interaction.locale, "player.skipped"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,16 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export const id = "player_stop"
export async function execute(interaction: ButtonInteraction) {
await stopProgressSaving(interaction.guild?.id ?? "", interaction.client.user.id)
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
queue.delete()
return interaction.followUp({ content: t(interaction.locale, "player.stopped"), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,14 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_volume_down"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
const volume = queue.node.volume - 10
queue.node.setVolume(volume)
return interaction.followUp({ content: t(interaction.locale, "player.volume_changed_down", { volume: volume.toString() }), flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,14 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const id = "player_volume_up"
export async function execute(interaction: ButtonInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return
const volume = queue.node.volume + 10
queue.node.setVolume(volume)
return interaction.followUp({ content: t(interaction.locale, "player.volume_changed", { volume: volume.toString() }), flags: MessageFlags.Ephemeral })
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useHistory } from 'discord-player'
export default {
id: 'previous',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let history = useHistory(guild.id)
if (!history) return
await history.previous()
return interaction.followUp({ content: 'Musique précédente jouée !', ephemeral: true })
}
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'resume',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
await queue.node.setPaused(!queue.node.isPaused())
return interaction.followUp({ content: 'Musique reprise !', ephemeral: true })
}
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'shuffle',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
await queue.tracks.shuffle()
return interaction.followUp({ content: 'File d\'attente mélangée !', ephemeral: true })
}
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'skip',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
await queue.node.skip()
return interaction.followUp({ content: 'Musique passée !', ephemeral: true })
}
}

View File

@@ -1,15 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'stop',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
await queue.delete()
return interaction.followUp({ content: 'Musique arrêtée !', ephemeral: true })
}
}

View File

@@ -0,0 +1,19 @@
import { MessageFlags, ChannelType, ActionRowBuilder, ChannelSelectMenuBuilder } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const id = "twitch_channel"
export async function execute(interaction: ButtonInteraction) {
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId("twitch_channel")
.setPlaceholder(t(interaction.locale, "twitch.select_notification_channel_placeholder"))
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
const row = new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(channelSelect)
return interaction.reply({
content: t(interaction.locale, "twitch.select_notification_channel"),
components: [row],
flags: MessageFlags.Ephemeral
})
}

View File

@@ -0,0 +1,28 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { generateTwitchEmbed } from "@/utils/twitch"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "twitch_disable"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
dbData.enabled = false
dbData.botId = ""
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
// Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour
const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.update({
embeds: [embed],
components: components
})
}

View File

@@ -0,0 +1,28 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { generateTwitchEmbed } from "@/utils/twitch"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "twitch_enable"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
dbData.enabled = true
dbData.botId = interaction.client.user.id
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
// Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour
const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.update({
embeds: [embed],
components: components
})
}

View File

@@ -0,0 +1,17 @@
import * as twitch_disable from "./disable"
import * as twitch_enable from "./enable"
import * as twitch_set_channel from "./channel"
import * as twitch_list_streamers from "./streamer_list"
import * as twitch_add_streamer from "./streamer_add"
import * as twitch_remove_streamer from "./streamer_remove"
import type { Button } from "@/types"
export default [
twitch_disable,
twitch_enable,
twitch_set_channel,
twitch_list_streamers,
twitch_add_streamer,
twitch_remove_streamer
] as Button[]

View File

@@ -0,0 +1,20 @@
import { MessageFlags } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const id = "twitch_streamer_add"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral })
if (!dbData.channelId) return interaction.reply({ content: t(interaction.locale, "twitch.configure_channel_first"), flags: MessageFlags.Ephemeral })
return interaction.reply({
content: t(interaction.locale, "twitch.add_streamer_command"),
flags: MessageFlags.Ephemeral
})
}

View File

@@ -0,0 +1,51 @@
import { MessageFlags, EmbedBuilder } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { twitchClient } from "@/utils/twitch"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
export const id = "twitch_streamer_list"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral })
if (!dbData.streamers.length) {
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "twitch.list.title"))
.setDescription(t(interaction.locale, "twitch.list.empty_description"))
.setColor(0x808080)
.setTimestamp()
return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral })
}
const streamers = [] as string[]
await Promise.all(dbData.streamers.map(async (streamer, index) => {
try {
const user = await twitchClient.users.getUserById(streamer.twitchUserId)
if (user) {
const discordUser = streamer.discordUserId ? `<@${streamer.discordUserId}>` : t(interaction.locale, "twitch.list.discord_not_associated")
streamers.push(`**${index + 1}.** ${user.displayName}\n└ Discord: ${discordUser}\n└ ID: \`${streamer.twitchUserId}\``)
} else {
streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.user_not_found")}\n└ ID: \`${streamer.twitchUserId}\``)
}
} catch (error) {
logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId })
console.error(error)
streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.fetch_error")}\n└ ID: \`${streamer.twitchUserId}\``)
}
}))
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "twitch.list.title"))
.setDescription(streamers.join("\n\n"))
.setColor(0x9146FF)
.setFooter({ text: t(interaction.locale, "twitch.list.footer", { count: streamers.length.toString() }) })
.setTimestamp()
return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral })
}

View File

@@ -0,0 +1,46 @@
import { MessageFlags, ActionRowBuilder, StringSelectMenuBuilder } from "discord.js"
import type { ButtonInteraction } from "discord.js"
import { twitchClient } from "@/utils/twitch"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
export const id = "twitch_streamer_remove"
export async function execute(interaction: ButtonInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral })
if (!dbData.streamers.length) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamers_list"), flags: MessageFlags.Ephemeral })
// Créer la liste des streamers dans un menu de sélection
const options = await Promise.all(dbData.streamers.map(async (streamer) => {
try {
const user = await twitchClient.users.getUserById(streamer.twitchUserId)
return {
label: user ? user.displayName : `ID: ${streamer.twitchUserId}`,
value: streamer.twitchUserId,
description: user ? `@${user.name}` : t(interaction.locale, "twitch.user_not_found")
}
} catch (error) {
logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId })
console.error(error)
return {
label: `ID: ${streamer.twitchUserId}`,
value: streamer.twitchUserId,
description: t(interaction.locale, "twitch.fetch_error")
}
}
}))
const selectMenu = new StringSelectMenuBuilder()
.setCustomId("twitch_streamer_remove")
.setPlaceholder(t(interaction.locale, "twitch.select_streamer_to_remove"))
.addOptions(options.slice(0, 25)) // Discord limite à 25 options
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
return interaction.reply({ content: t(interaction.locale, "twitch.select_streamer_prompt"), components: [row], flags: MessageFlags.Ephemeral })
}

View File

@@ -1,16 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'volume_down',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
let volume = queue.node.volume - 10
await queue.node.setVolume(volume)
return interaction.followUp({ content: `🔉 | Volume modifié à ${volume}% !`, ephemeral: true })
}
}

View File

@@ -1,16 +0,0 @@
import { ButtonInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
id: 'volume_up',
async execute(interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return
let queue = useQueue(guild.id)
if (!queue) return
let volume = queue.node.volume + 10
await queue.node.setVolume(volume)
return interaction.followUp({ content: `🔊 | Volume modifié à ${volume}% !`, ephemeral: true })
}
}

View File

@@ -1,210 +1,241 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, EmbedBuilder, inlineCode, PermissionFlagsBits } from 'discord.js'
import dbGuild from '../../schemas/guild'
import * as AMP from '../../utils/amp'
interface InstanceFields {
name: string
value: string
inline: boolean
}
interface InstanceResult {
status: string
data: [
Host
]
}
interface Host {
AvailableInstances: Instance[]
FriendlyName: string
}
interface Instance {
InstanceID: string
FriendlyName: string
Running: boolean
Module: string
Port: number
}
interface FailMsgData {
Title: string
Message: string
}
interface ErrorMsgData {
error_code: string
}
function failMsg(data: FailMsgData) { return `La commande a échouée !\n${inlineCode(`${data.Title}: ${data.Message}`)}` }
function errorMsg(data: ErrorMsgData) { return `Y'a eu une erreur !\n${inlineCode(`${data.error_code}`)}` }
export default {
data: new SlashCommandBuilder()
.setName('amp')
.setDescription('Accède à mon panel de jeu AMP !')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand.setName('login').setDescription("Connectez-vous avant d'effectuer une autre commande !")
.addStringOption(option => option.setName('username').setDescription("Nom d'Utilisateur").setRequired(true))
.addStringOption(option => option.setName('password').setDescription('Mot de Passe').setRequired(true))
.addBooleanOption(option => option.setName('remember').setDescription('Mémoriser les identifiants').setRequired(true))
.addStringOption(option => option.setName('otp').setDescription('Code de double authentification')))
.addSubcommandGroup(subcommandgroup => subcommandgroup.setName('instances').setDescription('Intéragir avec les instances AMP.')
.addSubcommand(subcommand => subcommand.setName('list').setDescription('Liste toutes les instances disponibles.'))
.addSubcommand(subcommand => subcommand.setName('manage').setDescription('Gérer une instance.')
.addStringOption(option => option.setName('instance').setDescription("Nom de l'instance").setRequired(true).setAutocomplete(true)))
.addSubcommand(subcommand => subcommand.setName('restart').setDescription('Redémarre une instance.')
.addStringOption(option => option.setName('name').setDescription("Nom de l'instance").setRequired(true)))
),
async autocompleteRun(interaction: AutocompleteInteraction) {
let query = interaction.options.getString('instance', true)
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id })
if (!guildProfile) return await interaction.respond([])
let dbData = guildProfile.get('guildAmp')
if (!dbData?.enabled) return await interaction.respond([])
let host = dbData.host as string
let username = dbData.username as string
let sessionID = dbData.sessionID as string
let rememberMeToken = dbData.rememberMeToken as string
// Check if the SessionID is still valid
let session = await AMP.CheckSession(host, sessionID)
if (session.status === 'fail') {
if (rememberMeToken) {
// Refresh the SessionID if the RememberMeToken is available
let details = { username, password: '', token: rememberMeToken, rememberMe: true }
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') sessionID = result.data.sessionID
else if (result.status === 'fail') return interaction.respond([])
else if (result.status === 'error') return interaction.respond([])
} else return await interaction.respond([])
}
else if (session.status === 'error') return interaction.respond([])
let choices: ApplicationCommandOptionChoiceData[] = []
let result = await AMP.ADSModule.GetInstances(host, sessionID)
if (result.status === 'success') {
let hosts = result.data.result as Host[]
hosts.forEach(host => {
let instances = host.AvailableInstances as Instance[]
instances.forEach(instance => {
if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID })
})
})
}
else if (result.status === 'fail') return interaction.respond([])
else if (result.status === 'error') return interaction.respond([])
return interaction.respond(choices)
},
async execute(interaction: ChatInputCommandInteraction) {
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id })
if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` })
let dbData = guildProfile.get('guildAmp')
if (!dbData?.enabled) return interaction.reply({ content: `AMP module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildAmp.enabled True\` !` })
let host = dbData.host as string
let username = dbData.username as string
let sessionID = dbData.sessionID as string
let rememberMeToken = dbData.rememberMeToken as string
// Let the user login
if (interaction.options.getSubcommand() == 'login') {
// Get a SessionID and a RememberMeToken if wanted
await interaction.deferReply({ ephemeral: true })
let details = {
username: interaction.options.getString('username') || '',
password: interaction.options.getString('password') || '',
rememberMe: interaction.options.getBoolean('remember') || '',
token: interaction.options.getString('otp') || ''
}
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') {
username = dbData['username'] = result.data.userInfo.Username
sessionID = dbData['sessionID'] = result.data.sessionID
rememberMeToken = dbData['rememberMeToken'] = result.data.rememberMeToken
guildProfile.set('guildAmp', dbData)
guildProfile.markModified('guildAmp')
await guildProfile.save().catch(console.error)
return await interaction.followUp(`Tu es connecté au panel sous **${username}** !`)
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
await interaction.deferReply()
// Check if the SessionID is still valid
let session = await AMP.CheckSession(host, sessionID)
if (session.status === 'fail') {
if (rememberMeToken) {
// Refresh the SessionID if the RememberMeToken is available
let details = { username, password: '', token: rememberMeToken, rememberMe: true }
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') sessionID = result.data.sessionID
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
else return await interaction.followUp(`Tu dois te connecter avant d'effectuer une autre commande !`)
}
else if (session.status === 'error') return await interaction.followUp(errorMsg(session.data))
if (interaction.options.getSubcommandGroup() == 'instances') {
if (interaction.options.getSubcommand() == 'list') {
let result = await AMP.ADSModule.GetInstances(host, sessionID) as InstanceResult
if (result.status === 'success') {
await interaction.followUp({ content: `${result.data.length} hôte(s) trouvé(s) !` })
result.data.forEach(async host => {
let fields = [] as InstanceFields[]
host.AvailableInstances.forEach((instance: Instance) => {
fields.push({
name: instance.FriendlyName,
value: `**Running:** ${instance.Running}\n**Port:** ${instance.Port}\n**Module:** ${instance.Module}`,
inline: true
})
})
let embed = new EmbedBuilder()
.setTitle(host.FriendlyName)
.setDescription(`Liste des ${host.AvailableInstances.length} instances :`)
.setColor(interaction.guild?.members.me?.displayColor || '#ffc370')
.setTimestamp()
.setFields(fields)
return await interaction.followUp({ embeds: [embed] })
})
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data as unknown as FailMsgData))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data as unknown as ErrorMsgData))
}
else if (interaction.options.getSubcommand() == 'manage') {
let instanceID = interaction.options.getString('instance', true)
let result = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID)
if (result.status === 'success') {
let server = await AMP.ADSModule.Servers(host, sessionID, instanceID)
if (server.status === 'success') return await interaction.followUp(`Ok !`)
else if (server.status === 'fail') return await interaction.followUp(failMsg(server.data))
else if (server.status === 'error') return await interaction.followUp(errorMsg(server.data))
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
else if (interaction.options.getSubcommand() == 'restart') {
let query = interaction.options.getString('name')
if (!query) return
let result = await AMP.ADSModule.RestartInstance(host, sessionID, query)
if (result.status === 'success') return await interaction.followUp(`Ok !`)
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
}
}
}
import { SlashCommandBuilder, EmbedBuilder, inlineCode, PermissionFlagsBits, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, Locale } from "discord.js"
import * as AMP from "@/utils/amp"
import type { Host, Instance, InstanceFields, InstanceResult, LoginSuccessData } from "@/types/amp"
import type { ReturnMsgData } from "@/types"
import type { GuildAmp } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
function returnMsg(result: ReturnMsgData, locale: Locale) {
if (result.status === "fail") return `${t(locale, "common.failed")}\n${inlineCode(`${result.Title}: ${result.Message}`)}`
if (result.status === "error") return `${t(locale, "common.error_occurred")}\n${inlineCode(`${result.error_code}`)}`
}
export const data = new SlashCommandBuilder()
.setName("amp")
.setDescription("Access my AMP gaming panel")
.setDescriptionLocalizations({ fr: "Accède à mon panel de jeu AMP" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand
.setName("login")
.setDescription("Log in before performing another command")
.setNameLocalizations({ fr: "connexion" })
.setDescriptionLocalizations({ fr: "Connectez-vous avant d'effectuer une autre commande" })
.addStringOption(option => option
.setName("username")
.setDescription("Username")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Nom d'utilisateur" })
.setRequired(true)
)
.addStringOption(option => option
.setName("password")
.setDescription("Password")
.setNameLocalizations({ fr: "mot_de_passe" })
.setDescriptionLocalizations({ fr: "Mot de passe" })
.setRequired(true)
)
.addBooleanOption(option => option
.setName("remember")
.setDescription("Remember credentials")
.setNameLocalizations({ fr: "memoriser" })
.setDescriptionLocalizations({ fr: "Mémoriser les identifiants" })
.setRequired(true)
)
.addStringOption(option => option
.setName("otp")
.setDescription("Two-factor authentication code")
.setNameLocalizations({ fr: "otp" })
.setDescriptionLocalizations({ fr: "Code d'authentification à 2 facteurs" })
)
)
.addSubcommandGroup(subcommandgroup => subcommandgroup
.setName("instances")
.setDescription("Interact with AMP instances")
.setNameLocalizations({ fr: "instances" })
.setDescriptionLocalizations({ fr: "Intéragir avec les instances AMP" })
.addSubcommand(subcommand => subcommand
.setName("list")
.setDescription("List all available instances")
.setNameLocalizations({ fr: "liste" })
.setDescriptionLocalizations({ fr: "Lister toutes les instances disponibles" })
)
.addSubcommand(subcommand => subcommand
.setName("manage")
.setDescription("Manage an instance")
.setNameLocalizations({ fr: "gerer" })
.setDescriptionLocalizations({ fr: "Gérer une instance" })
.addStringOption(option => option
.setName("instance")
.setDescription("Instance name")
.setNameLocalizations({ fr: "instance" })
.setDescriptionLocalizations({ fr: "Nom de l'instance" })
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand(subcommand => subcommand
.setName("restart")
.setDescription("Restart an instance")
.setNameLocalizations({ fr: "redemarrer" })
.setDescriptionLocalizations({ fr: "Redémarrer une instance" })
.addStringOption(option => option
.setName("name")
.setDescription("Instance name")
.setNameLocalizations({ fr: "nom" })
.setDescriptionLocalizations({ fr: "Nom de l'instance" })
.setRequired(true)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildAmp") as GuildAmp
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral })
const host = dbData.host
if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral })
let username = dbData.username
let sessionID = dbData.sessionID
let rememberMeToken = dbData.rememberMeToken
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand == "login") {
// Get a SessionID and a RememberMeToken if wanted
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
const details = {
username: interaction.options.getString("username", true),
password: interaction.options.getString("password", true),
token: interaction.options.getString("otp") ?? "",
rememberMe: interaction.options.getBoolean("remember", true)
}
const result = await AMP.Core.Login(host, details)
if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral })
const loginData = result.data as LoginSuccessData
username = dbData.username = loginData.userInfo.Username
sessionID = dbData.sessionID = loginData.sessionID
rememberMeToken = dbData.rememberMeToken = loginData.rememberMeToken
guildProfile.set("guildAmp", dbData)
guildProfile.markModified("guildAmp")
await guildProfile.save().catch(console.error)
return interaction.followUp({ content: t(interaction.locale, "amp.logged_in", { username }), flags: MessageFlags.Ephemeral })
}
await interaction.deferReply()
// Check if the SessionID is still valid
if (!sessionID) return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
const checkResult = await AMP.CheckSession(host, sessionID)
if (checkResult.status === "fail") {
if (rememberMeToken && username) {
// Refresh the SessionID if the RememberMeToken is available
const details = { username, password: "", token: rememberMeToken, rememberMe: true }
const loginResult = await AMP.Core.Login(host, details)
if (loginResult.status !== "success") return interaction.followUp({ content: returnMsg(loginResult, interaction.locale), flags: MessageFlags.Ephemeral })
const loginData = loginResult.data as LoginSuccessData
sessionID = loginData.sessionID
}
else return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
}
else if (checkResult.status === "error") return interaction.followUp({ content: returnMsg(checkResult, interaction.locale), flags: MessageFlags.Ephemeral })
if (subcommandGroup == "instances") {
if (subcommand == "list") {
const result = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult
if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral })
await interaction.followUp({ content: t(interaction.locale, "amp.hosts_found", { count: result.data.length }) })
await Promise.all(result.data.map(async host => {
const fields = [] as InstanceFields[]
host.AvailableInstances.forEach((instance: Instance) => {
fields.push({ name: instance.FriendlyName, value: `**${t(interaction.locale, "amp.running")}:** ${instance.Running}\n**${t(interaction.locale, "amp.port")}:** ${instance.Port}\n**${t(interaction.locale, "amp.module")}:** ${instance.Module}`, inline: true })
})
const embed = new EmbedBuilder()
.setTitle(host.FriendlyName)
.setDescription(t(interaction.locale, "amp.instance_list", { count: host.AvailableInstances.length }))
.setColor(interaction.guild?.members.me?.displayColor ?? "#ffc370")
.setTimestamp()
.setFields(fields)
return interaction.followUp({ embeds: [embed] })
}))
}
else if (subcommand == "manage") {
const instanceID = interaction.options.getString("instance", true)
const manageResult = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID)
if (manageResult.status !== "success") return interaction.followUp({ content: returnMsg(manageResult, interaction.locale), flags: MessageFlags.Ephemeral })
const serversResult = await AMP.ADSModule.Servers(host, sessionID, instanceID)
if (serversResult.status !== "success") return interaction.followUp({ content: returnMsg(serversResult, interaction.locale), flags: MessageFlags.Ephemeral })
return interaction.followUp({ content: t(interaction.locale, "amp.manage_success") })
}
else if (subcommand == "restart") {
const query = interaction.options.getString("name", true)
const restartResult = await AMP.ADSModule.RestartInstance(host, sessionID, query)
if (restartResult.status !== "success") return interaction.followUp({ content: returnMsg(restartResult, interaction.locale), flags: MessageFlags.Ephemeral })
return interaction.followUp({ content: t(interaction.locale, "amp.restart_success") })
}
}
}
export async function autocompleteRun(interaction: AutocompleteInteraction) {
const query = interaction.options.getString("instance", true)
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.respond([])
const dbData = guildProfile.get("guildAmp") as GuildAmp
if (!dbData.enabled) return interaction.respond([])
const host = dbData.host
if (!host) return interaction.respond([])
let sessionID = dbData.sessionID
if (!sessionID) return interaction.respond([])
const username = dbData.username
const rememberMeToken = dbData.rememberMeToken
const checkResult = await AMP.CheckSession(host, sessionID)
if (checkResult.status === "fail") {
if (rememberMeToken && username) {
// Refresh the SessionID if the RememberMeToken is available
const details = { username, password: "", token: rememberMeToken, rememberMe: true }
const loginResult = await AMP.Core.Login(host, details)
if (loginResult.status !== "success") return interaction.respond([])
const loginData = loginResult.data as LoginSuccessData
sessionID = loginData.sessionID
}
else return interaction.respond([])
}
else if (checkResult.status === "error") return interaction.respond([])
const instancesResult = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult
if (instancesResult.status !== "success") return interaction.respond([])
const choices: ApplicationCommandOptionChoiceData[] = []
const hosts = instancesResult.data as Host[]
hosts.forEach(host => {
const instances = host.AvailableInstances
instances.forEach(instance => {
if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID })
})
})
return interaction.respond(choices)
}

View File

@@ -1,37 +1,39 @@
import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, TextChannel, PermissionFlagsBits } from 'discord.js'
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
module.exports = {
data: new SlashCommandBuilder()
.setName('boost')
.setDescription('Tester le boost du serveur !')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction: ChatInputCommandInteraction) {
if (interaction.guild?.id !== '796327643783626782') return interaction.reply({ content: 'Non !' })// Jujul Community
let member = interaction.member
if (!member) return console.log(`\u001b[1;31m Aucun membre trouvé !`)
export const data = new SlashCommandBuilder()
.setName("boost")
.setDescription("Test the server boost")
.setDescriptionLocalizations({ fr: "Tester le boost du serveur" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
let guild = interaction.guild
if (!guild) return console.log(`\u001b[1;31m Aucun serveur trouvé !`)
export async function execute(interaction: ChatInputCommandInteraction) {
if (interaction.guild?.id !== "796327643783626782") return interaction.reply({ content: t(interaction.locale, "boost.not_authorized") }) // Jujul Community
let channel = guild.channels.cache.get('924353449930412153') as TextChannel
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`)
const member = interaction.member
if (!member) { logConsole('discordjs', 'boost.no_member'); return }
let boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`)
const guild = interaction.guild
if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return }
if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`)
let embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(`Nouveau boost de ${member.user.username} !`)
.setDescription(`
Merci à toi pour ce boost.\n
Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts !
`)
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
await interaction.reply({ content: 'Va voir dans <#924353449930412153> !' })
const channel = await guild.channels.fetch("924353449930412153")
if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" })
return
}
}
const boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return }
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(t(interaction.locale, "boost.new_boost_title", { username: member.user.username }))
.setDescription(t(interaction.locale, "boost.new_boost_description", { count: (guild.premiumSubscriptionCount ?? 0).toString() }))
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
return interaction.reply({ content: t(interaction.locale, "boost.check_channel", { channelId: "924353449930412153" }) })
}

View File

@@ -1,76 +1,108 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, APIEmbedField, PermissionFlagsBits } from 'discord.js'
import dbGuildInit from '../../utils/dbGuildInit'
import dbGuild from '../../schemas/guild'
const parseObject = (obj: object, prefix = ''): { name: string, value: object | string | boolean }[] => {
let fields: { name: string, value: object | string | boolean }[] = []
for (let [key, value] of Object.entries(obj)) {
if (typeof value === 'object') fields.push(...parseObject(value, `${prefix}${key}.`))
else {
if (typeof value === 'boolean') value = value ? 'True' : 'False'
else if (!value) value = 'None'
else value = value.toString()
fields.push({ name: `${prefix}${key}`, value })
}
}
return fields
}
export default {
data: new SlashCommandBuilder()
.setName('database')
.setDescription('Communicate with the database')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand.setName('info').setDescription('Returns information about the current guild'))
.addSubcommand(subcommand => subcommand.setName('init').setDescription('Force initialize an entry for the current guild in the database'))
.addSubcommand(subcommand => subcommand.setName('edit').setDescription('Modify parameters for the current guild')
.addStringOption(option => option.setName('key').setDescription('Key to modify').setRequired(true))
.addStringOption(option => option.setName('value').setDescription('Value to set').setRequired(true))),
async execute(interaction: ChatInputCommandInteraction) {
let guild = interaction.guild
if (!guild) return await interaction.reply({ content: 'This command must be used in a server.', ephemeral: true })
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (interaction.options.getSubcommand() === 'info') {
if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist !` })
let fields = parseObject(guildProfile.toObject())
let embed = new EmbedBuilder()
.setTitle('Database Information')
.setDescription(`Guild **${guildProfile.guildName}** (ID: ${guildProfile.guildId})`)
.setThumbnail(guildProfile.guildIcon as string)
.setTimestamp()
//.addFields(fields as APIEmbedField[])
// Limit the number of fields to 25
.addFields(fields.slice(0, 25) as APIEmbedField[])
return await interaction.reply({ embeds: [embed] })
} else if (interaction.options.getSubcommand() === 'init') {
if (guildProfile) return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** already exists !` })
guildProfile = await dbGuildInit(guild)
if (!guildProfile) return await interaction.reply({ content: `An error occured while initializing database data for **${guild.name}** !` })
return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully initialized !` })
} else if (interaction.options.getSubcommand() === 'edit') {
if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist, please init with \`/database init\` !` })
let key = interaction.options.getString('key', true)
let value = interaction.options.getString('value', true)
let oldValue = guildProfile.get(key)
if (!oldValue) oldValue = 'None'
guildProfile.set(key, value)
await guildProfile.save().catch(console.error)
return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully updated !\n**${key}**: ${oldValue} -> ${value}` })
}
}
}
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, APIEmbedField } from "discord.js"
import dbGuildInit from "@/utils/dbGuildInit"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
const parseObject = (obj: object, prefix = ""): { name: string, value: object | string | boolean }[] => {
const fields: { name: string, value: object | string | boolean }[] = []
for (const [key, value] of Object.entries(obj)) {
if (value !== null && typeof value === "object") fields.push(...parseObject(value as object, `${prefix}${key}.`))
else {
let newValue: string
if (typeof value === "boolean") newValue = value ? "True" : "False"
else if (value === null || value === undefined) newValue = "None"
else newValue = String(value)
fields.push({ name: `${prefix}${key}`, value: newValue })
}
}
return fields
}
export const data = new SlashCommandBuilder()
.setName("database")
.setDescription("Communicate with the database")
.setDescriptionLocalizations({ fr: "Communiquer avec la base de données" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand
.setName("info")
.setDescription("Returns information about the current guild")
.setNameLocalizations({ fr: "info" })
.setDescriptionLocalizations({ fr: "Retourne les informations sur le serveur actuel" })
)
.addSubcommand(subcommand => subcommand
.setName("init")
.setDescription("Force initialize an entry for the current guild in the database")
.setNameLocalizations({ fr: "init" })
.setDescriptionLocalizations({ fr: "Initialiser de force une entrée pour le serveur actuel dans la base de données" })
)
.addSubcommand(subcommand => subcommand
.setName("edit")
.setDescription("Modify parameters for the current guild")
.setNameLocalizations({ fr: "modifier" })
.setDescriptionLocalizations({ fr: "Modifier les paramètres pour le serveur actuel" })
.addStringOption(option => option
.setName("key")
.setDescription("Key to modify")
.setNameLocalizations({ fr: "cle" })
.setDescriptionLocalizations({ fr: "Clé à modifier" })
.setRequired(true)
)
.addStringOption(option => option
.setName("value")
.setDescription("Value to set")
.setNameLocalizations({ fr: "valeur" })
.setDescriptionLocalizations({ fr: "Valeur à définir" })
.setRequired(true)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
if (interaction.user !== interaction.client.application.owner) return interaction.reply({ content: t(interaction.locale, "database.owner_only"), flags: MessageFlags.Ephemeral })
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "database.server_only"), flags: MessageFlags.Ephemeral })
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
const subcommand = interaction.options.getSubcommand(true)
if (subcommand === "info") {
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const fields = parseObject(guildProfile.toObject())
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "database.info_title"))
.setDescription(t(interaction.locale, "database.guild_info", { name: guildProfile.guildName, id: guildProfile.guildId }))
.setThumbnail(guildProfile.guildIcon)
.setTimestamp()
//.addFields(fields as APIEmbedField[])
// Limit the number of fields to 25
.addFields(fields.slice(0, 25) as APIEmbedField[])
return interaction.reply({ embeds: [embed] })
}
else if (subcommand === "init") {
if (guildProfile) return interaction.reply({ content: t(interaction.locale, "database.already_exists", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral })
guildProfile = await dbGuildInit(guild)
return interaction.reply({ content: t(interaction.locale, "database.initialized", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "edit") {
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const key = interaction.options.getString("key", true)
const value = interaction.options.getString("value", true)
let oldValue: string = guildProfile.get(key) as string
if (!oldValue) oldValue = t(interaction.locale, "common.none")
guildProfile.set(key, value)
guildProfile.markModified(key)
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "database.updated", { name: guildProfile.guildName, key, oldValue, value }), flags: MessageFlags.Ephemeral })
}
}

View File

@@ -0,0 +1,353 @@
import { SlashCommandBuilder, EmbedBuilder, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, inlineCode } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import crypto from "crypto"
import * as Freebox from "@/utils/freebox"
import type {
APIResponseData, APIResponseDataError, APIResponseDataVersion,
ConnectionStatus, GetChallenge, LcdConfig, OpenSession, RequestAuthorization, TrackAuthorizationProgress
} from "@/types/freebox"
import type { GuildFbx } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("freebox")
.setDescription("Access FreeboxOS API")
.setDescriptionLocalizations({ fr: "Accéder à l'API FreeboxOS" })
.addSubcommand(subcommand => subcommand
.setName("status")
.setDescription("Display Freebox configuration and status")
.setDescriptionLocalizations({ fr: "Afficher la configuration et l'état de la Freebox" })
)
.addSubcommand(subcommand => subcommand
.setName("setup")
.setDescription("Configure Freebox settings easily")
.setDescriptionLocalizations({ fr: "Configurer facilement les paramètres Freebox" })
.addStringOption(option => option
.setName("host")
.setDescription("Freebox host (IP address)")
.setNameLocalizations({ fr: "hote" })
.setDescriptionLocalizations({ fr: "Hôte Freebox (adresse IP)" })
.setRequired(false)
)
.addIntegerOption(option => option
.setName("version")
.setDescription("Freebox API version")
.setNameLocalizations({ fr: "version" })
.setDescriptionLocalizations({ fr: "Version de l'API Freebox" })
.setRequired(false)
)
.addBooleanOption(option => option
.setName("enabled")
.setDescription("Enable or disable the Freebox module")
.setNameLocalizations({ fr: "active" })
.setDescriptionLocalizations({ fr: "Activer ou désactiver le module Freebox" })
.setRequired(false)
)
)
.addSubcommand(subcommand => subcommand
.setName("init")
.setDescription("Create an app on the Freebox to authenticate")
.setDescriptionLocalizations({ fr: "Créer une app sur la Freebox pour s'authentifier" })
.addStringOption(option => option
.setName("host")
.setDescription("Freebox host (IP or domain)")
.setNameLocalizations({ fr: "hote" })
.setDescriptionLocalizations({ fr: "Hôte Freebox (IP ou domaine)" })
.setRequired(true)
)
)
.addSubcommandGroup(subcommandGroup => subcommandGroup
.setName("get")
.setDescription("Retrieve data")
.setNameLocalizations({ fr: "recuperer" })
.setDescriptionLocalizations({ fr: "Récupérer des données" })
.addSubcommand(subcommand => subcommand
.setName("version")
.setDescription("Display API version")
.setDescriptionLocalizations({ fr: "Afficher la version de l'API" })
)
.addSubcommand(subcommand => subcommand
.setName("connection")
.setDescription("Retrieve connection information")
.setNameLocalizations({ fr: "connexion" })
.setDescriptionLocalizations({ fr: "Récupérer les informations de connexion" })
)
.addSubcommand(subcommand => subcommand
.setName("lcd")
.setDescription("Retrieve LCD configuration")
.setDescriptionLocalizations({ fr: "Récupérer la configuration de l'écran LCD" })
)
)
.addSubcommandGroup(subcommandGroup => subcommandGroup
.setName("lcd")
.setDescription("Control LCD features")
.setDescriptionLocalizations({ fr: "Contrôler les fonctionnalités LCD" })
.addSubcommand(subcommand => subcommand
.setName("leds")
.setDescription("Toggle LED strip on/off")
.setNameLocalizations({ fr: "leds" })
.setDescriptionLocalizations({ fr: "Allumer/éteindre le bandeau LED" })
.addBooleanOption(option => option
.setName("enabled")
.setDescription("Enable or disable LED strip")
.setNameLocalizations({ fr: "active" })
.setDescriptionLocalizations({ fr: "Activer ou désactiver le bandeau LED" })
.setRequired(true)
)
)
.addSubcommand(subcommand => subcommand
.setName("timer")
.setDescription("Setup automatic LED timer")
.setNameLocalizations({ fr: "minuteur" })
.setDescriptionLocalizations({ fr: "Configurer le minuteur automatique des LEDs" })
.addStringOption(option => option
.setName("action")
.setDescription("Timer action")
.setNameLocalizations({ fr: "action" })
.setDescriptionLocalizations({ fr: "Action du minuteur" })
.setRequired(true)
.addChoices(
{ name: "Enable timer", value: "enable", name_localizations: { fr: "Activer minuteur" } },
{ name: "Disable timer", value: "disable", name_localizations: { fr: "Désactiver minuteur" } },
{ name: "Status", value: "status", name_localizations: { fr: "Statut" } }
)
)
.addStringOption(option => option
.setName("morning_time")
.setDescription("Morning time (HH:MM format, 24h)")
.setNameLocalizations({ fr: "heure_matin" })
.setDescriptionLocalizations({ fr: "Heure du matin (format HH:MM, 24h)" })
.setRequired(false)
)
.addStringOption(option => option
.setName("night_time")
.setDescription("Night time (HH:MM format, 24h)")
.setNameLocalizations({ fr: "heure_nuit" })
.setDescriptionLocalizations({ fr: "Heure du soir (format HH:MM, 24h)" })
.setRequired(false)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildFbx") as GuildFbx
let host = dbData.host
let appToken = dbData.appToken
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand === "status") {
// Construire l'embed de statut
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "freebox.status.title"))
.setColor(dbData.enabled ? 0x00ff00 : 0xff0000)
.addFields({
name: t(interaction.locale, "freebox.status.config_section"),
value: [
t(interaction.locale, "freebox.status.module_field", { status: dbData.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }),
t(interaction.locale, "freebox.status.host_field", { value: host ? `\`${host}\`` : t(interaction.locale, "freebox.status.host_not_configured") }),
t(interaction.locale, "freebox.status.token_field", { status: appToken ? t(interaction.locale, "freebox.status.token_configured") : t(interaction.locale, "freebox.status.token_not_configured") })
].join('\n'),
inline: false
})
// Informations LCD si disponibles
if (dbData.lcd) {
const lcdStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.status.timer_enabled") : t(interaction.locale, "freebox.status.timer_disabled")
const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager")
const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
embed.addFields({
name: t(interaction.locale, "freebox.status.timer_section"),
value: [
t(interaction.locale, "freebox.timer.status_field", { status: lcdStatus }),
t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }),
t(interaction.locale, "freebox.timer.morning", { time: morningTime }),
t(interaction.locale, "freebox.timer.night", { time: nightTime })
].join('\n'),
inline: false
})
}
// Boutons d'action
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("freebox_test_connection")
.setLabel(t(interaction.locale, "freebox.buttons.test_connection"))
.setEmoji("🔌")
.setStyle(ButtonStyle.Primary)
.setDisabled(!appToken),
new ButtonBuilder()
.setCustomId("freebox_lcd_status")
.setLabel(t(interaction.locale, "freebox.buttons.lcd_status"))
.setEmoji("💡")
.setStyle(ButtonStyle.Secondary)
.setDisabled(!appToken),
new ButtonBuilder()
.setCustomId("freebox_refresh_status")
.setLabel(t(interaction.locale, "freebox.buttons.refresh_status"))
.setEmoji("🔄")
.setStyle(ButtonStyle.Success)
)
return interaction.reply({ embeds: [embed], components: [buttons], flags: MessageFlags.Ephemeral })
}
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "common.module_disabled"), flags: MessageFlags.Ephemeral })
if (subcommand === "init") {
if (appToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.app_token_already_set"), flags: MessageFlags.Ephemeral })
host = interaction.options.getString("host", true)
if (host === "mafreebox.freebox.fr") return interaction.reply({ content: t(interaction.locale, "freebox.auth.host_not_allowed"), flags: MessageFlags.Ephemeral })
const initData = await Freebox.Core.Init(host) as APIResponseData<RequestAuthorization>
if (!initData.success) return Freebox.handleError(initData as APIResponseDataError, interaction)
appToken = initData.result.app_token
const trackId = initData.result.track_id
if (!trackId) return interaction.reply({ content: t(interaction.locale, "freebox.auth.track_id_failed"), flags: MessageFlags.Ephemeral })
// Si l'utilisateur n'a pas encore autorisé l'application, on lui demande de le faire
await interaction.reply({ content: t(interaction.locale, "freebox.auth.init_in_progress", { trackId }), flags: MessageFlags.Ephemeral })
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const initCheck = setInterval(async () => {
if (!host || !trackId) { clearInterval(initCheck); return }
const trackData = await Freebox.Core.Init(host, trackId) as APIResponseData<TrackAuthorizationProgress>
if (!trackData.success) return Freebox.handleError(trackData as APIResponseDataError, interaction, false)
const status = trackData.result.status
if (status === "granted") {
clearInterval(initCheck)
dbData.appToken = appToken
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
return interaction.followUp({ content: t(interaction.locale, "common.success"), flags: MessageFlags.Ephemeral })
} else if (status === "denied") {
clearInterval(initCheck)
return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral })
} else if (status === "pending") { console.log("Freebox authorization pending...") }
}, 2000)
}
else {
if (!host) return interaction.reply({ content: t(interaction.locale, "freebox.general.host_not_set"), flags: MessageFlags.Ephemeral })
if (subcommand === "version") {
const versionData = await Freebox.Core.Version(host) as APIResponseDataVersion
const embed = new EmbedBuilder()
.setTitle("FreeboxOS API Version")
.setDescription(`Version: ${versionData.api_version || "Unknown"}`)
return interaction.reply({ embeds: [embed] })
}
if (!appToken) return interaction.reply({ content: t(interaction.locale, "freebox.general.app_token_not_set"), flags: MessageFlags.Ephemeral })
const challengeData = await Freebox.Login.Challenge(host) as APIResponseData<GetChallenge>
if (!challengeData.success) return Freebox.handleError(challengeData as APIResponseDataError, interaction)
const password = crypto.createHmac("sha1", appToken).update(challengeData.result.challenge).digest("hex")
const sessionData = await Freebox.Login.Session(host, password) as APIResponseData<OpenSession>
if (!sessionData.success) return Freebox.handleError(sessionData as APIResponseDataError, interaction)
const sessionToken = sessionData.result.session_token
if (!sessionToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral })
if (subcommandGroup === "get") {
if (subcommand === "connection") {
const connectionData = await Freebox.Get.Connection(host, sessionToken) as APIResponseData<ConnectionStatus>
if (!connectionData.success) return Freebox.handleError(connectionData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.api.connection_details", { details: inlineCode(JSON.stringify(connectionData.result)) }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "lcd") {
const lcdData = await Freebox.Get.LcdConfig(host, sessionToken) as APIResponseData<LcdConfig>
if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.api.lcd_details", { details: inlineCode(JSON.stringify(lcdData.result)) }), flags: MessageFlags.Ephemeral })
}
}
else if (subcommandGroup === "lcd") {
// Initialiser l'objet LCD s'il n'existe pas
dbData.lcd ??= { enabled: false }
// Vérifier si le bot est autorisé pour ce serveur
if (dbData.lcd.enabled && dbData.lcd.botId && dbData.lcd.botId !== interaction.client.user.id) {
return interaction.reply({ content: t(interaction.locale, "freebox.lcd.managed_by_other_bot"), flags: MessageFlags.Ephemeral })
}
if (subcommand === "leds") {
const enabled = interaction.options.getBoolean("enabled", true)
const lcdData = await Freebox.Set.LcdConfig(host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData<LcdConfig>
if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.lcd.leds_success", { status: enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "timer") {
const action = interaction.options.getString("action")
if (!action) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_action"), flags: MessageFlags.Ephemeral })
if (action === "status") {
const status = dbData.lcd.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled")
const managedBy = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "common.none")
return interaction.reply({ content: t(interaction.locale, "freebox.timer.status_display", { status, managedBy }), flags: MessageFlags.Ephemeral })
}
else if (action === "enable") {
const morningTime = interaction.options.getString("morning_time")
const nightTime = interaction.options.getString("night_time")
if (!morningTime || !nightTime) return interaction.reply({ content: t(interaction.locale, "freebox.timer.times_required"), flags: MessageFlags.Ephemeral })
// Valider le format HH:MM
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/
if (!timeRegex.test(morningTime) || !timeRegex.test(nightTime)) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_time_format"), flags: MessageFlags.Ephemeral })
// Activer le timer et enregistrer ce bot comme responsable
dbData.lcd.enabled = true
dbData.lcd.botId = interaction.client.user.id
dbData.lcd.morningTime = morningTime
dbData.lcd.nightTime = nightTime
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
// Démarrer les timers automatiques
if (interaction.guildId) Freebox.Timer.schedule(interaction.client, interaction.guildId, dbData)
return interaction.reply({ content: t(interaction.locale, "freebox.timer.enabled", { morningTime, nightTime }), flags: MessageFlags.Ephemeral })
}
else if (action === "disable") {
// Arrêter les timers actifs avant de désactiver
if (interaction.guildId) Freebox.Timer.clear(interaction.guildId)
// Désactiver le timer
dbData.lcd.enabled = false
dbData.lcd.botId = ""
dbData.lcd.morningTime = ""
dbData.lcd.nightTime = ""
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "freebox.timer.disabled"), flags: MessageFlags.Ephemeral })
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
import * as amp from "./amp"
import * as boost from "./boost"
import * as database from "./database"
import * as freebox from "./freebox"
import * as ping from "./ping"
import * as twitch from "./twitch"
import type { Command } from "@/types"
export default [
amp,
boost,
database,
freebox,
ping,
twitch
] as Command[]

View File

@@ -1,11 +1,17 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
export default {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check the latency of the bot'),
async execute(interaction: ChatInputCommandInteraction) {
let sent = await interaction.reply({ content: 'Pinging...', fetchReply: true })
interaction.editReply(`Websocket heartbeat: ${interaction.client.ws.ping}ms.\nRoundtrip latency: ${sent.createdTimestamp - interaction.createdTimestamp}ms`)
}
}
import { SlashCommandBuilder } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Check the latency of the bot")
.setDescriptionLocalizations({ fr: "Vérifier la latence du bot" })
export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.reply({ content: t(interaction.locale, "ping.pinging") })
const sent = await interaction.fetchReply()
return interaction.editReply(t(interaction.locale, "ping.response", {
heartbeat: interaction.client.ws.ping.toString(),
latency: (sent.createdTimestamp - interaction.createdTimestamp).toString()
}))
}

View File

@@ -0,0 +1,198 @@
import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js"
import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js"
import chalk from "chalk"
import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("twitch")
.setDescription("Manage streamers notifications")
.setDescriptionLocalizations({ fr: "Gérer les notifications des streameurs" })
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand => subcommand
.setName("status")
.setDescription("Display Twitch module status")
.setNameLocalizations({ fr: "statut" })
.setDescriptionLocalizations({ fr: "Afficher le statut du module Twitch" })
)
.addSubcommand(subcommand => subcommand
.setName("channel")
.setDescription("Set the channel to send notifications")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Définir le canal pour envoyer les notifications" })
.addChannelOption(option => option
.setName("channel")
.setDescription("The channel to send notifications")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Le canal pour envoyer les notifications" })
.setRequired(true)
)
)
.addSubcommandGroup(subcommandgroup => subcommandgroup
.setName("streamer")
.setDescription("Manage streamers")
.setNameLocalizations({ fr: "streameur" })
.setDescriptionLocalizations({ fr: "Gérer les streameurs" })
.addSubcommand(subcommand => subcommand
.setName("list")
.setDescription("List all streamers")
.setNameLocalizations({ fr: "liste" })
.setDescriptionLocalizations({ fr: "Lister tous les streameurs" })
)
.addSubcommand(subcommand => subcommand
.setName("add")
.setDescription("Add a streamer")
.setNameLocalizations({ fr: "ajouter" })
.setDescriptionLocalizations({ fr: "Ajouter un streameur" })
.addStringOption(option => option
.setName("username")
.setDescription("The username of the streamer to add")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à ajouter" })
.setRequired(true)
.setAutocomplete(true)
)
.addUserOption(option => option
.setName("member")
.setDescription("The member on the guild to mention")
.setNameLocalizations({ fr: "membre" })
.setDescriptionLocalizations({ fr: "Le membre sur le serveur à mentionner" })
.setRequired(false)
)
)
.addSubcommand(subcommand => subcommand
.setName("remove")
.setDescription("Remove a streamer")
.setNameLocalizations({ fr: "supprimer" })
.setDescriptionLocalizations({ fr: "Supprimer un streameur" })
.addStringOption(option => option
.setName("username")
.setDescription("The username of the streamer to remove")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à supprimer" })
.setRequired(true)
.setAutocomplete(true)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand == "status") {
// Utiliser la fonction utilitaire pour générer l'embed et les composants
const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.reply({ embeds: [embed], components: components, flags: MessageFlags.Ephemeral })
}
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled_activate"), flags: MessageFlags.Ephemeral })
if (subcommand == "channel") {
const channel = interaction.options.getChannel("channel", true)
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)
return interaction.reply({ content: t(interaction.locale, "common.invalid_text_channel"), flags: MessageFlags.Ephemeral })
dbData.channelId = channel.id
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "twitch.notifications_channel_set", { channel: channel.name ?? channel.id }), flags: MessageFlags.Ephemeral })
}
else if (subcommandGroup == "streamer") {
if (!dbData.channelId) return interaction.reply({ content: t(interaction.locale, "twitch.configure_channel_first"), flags: MessageFlags.Ephemeral })
if (subcommand == "list") {
if (!dbData.streamers.length) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamers_list"), flags: MessageFlags.Ephemeral })
const streamers = [] as string[]
await Promise.all(dbData.streamers.map(async streamer => {
try {
const user = await twitchClient.users.getUserById(streamer.twitchUserId)
if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`)
else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`)
} catch (error) {
console.log(chalk.magenta(`[Twitch] Error fetching user for ID ${streamer.twitchUserId}`))
console.error(error)
}
}))
const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers")
return interaction.reply({ content: `${t(interaction.locale, "twitch.list.title")}:\n${streamerList}`, flags: MessageFlags.Ephemeral })
}
else if (subcommand == "add") {
const username = interaction.options.getString("username", true)
const member = interaction.options.getUser("member", false)
const user = await twitchClient.users.getUserByName(username)
if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral })
if (dbData.streamers.some(s => s.twitchUserId === user.id)) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_already_added", { username }), flags: MessageFlags.Ephemeral })
dbData.streamers.push({ twitchUserId: user.id, discordUserId: member?.id ?? "", messageId: "" })
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) listener.onStreamOnline(user.id, onlineSub)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) listener.onStreamOffline(user.id, offlineSub)
return interaction.reply({ content: t(interaction.locale, "twitch.streamer_added", { username, id: user.id }), flags: MessageFlags.Ephemeral })
}
else if (subcommand == "remove") {
const username = interaction.options.getString("username", true)
const user = await twitchClient.users.getUserByName(username)
if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral })
const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === user.id)
if (streamerIndex === -1)return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_in_list", { username }), flags: MessageFlags.Ephemeral })
dbData.streamers.splice(streamerIndex, 1)
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) {
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id)
await Promise.all(userSubs.data.map(async sub => { if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() }))
console.log(chalk.magenta(`[Twitch] Listener removed for ${user.displayName} (ID ${user.id})`))
}
return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral })
}
}
}
export async function autocompleteRun(interaction: AutocompleteInteraction) {
const query = interaction.options.getString("username", true)
if (!query || query.length < 3) return interaction.respond([])
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.respond([])
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbData.enabled) return interaction.respond([])
const choices: ApplicationCommandOptionChoiceData[] = []
const searchResult = await twitchClient.search.searchChannels(query)
if (searchResult.data.length === 0) return interaction.respond([])
searchResult.data.forEach(streamerResult => {
if (dbData.streamers.some(s => s.twitchUserId === streamerResult.id)) return
choices.push({ name: streamerResult.displayName, value: streamerResult.name })
})
return interaction.respond(choices)
}

26
src/commands/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import global from "./global"
import player from "./player"
import salonpostam from "./salonpostam"
import { Command, CommandFolder } from "@/types"
export const commandFolders = [
{
name: "global",
commands: global
},
{
name: "player",
commands: player
},
{
name: "salonpostam",
commands: salonpostam
}
] as CommandFolder[]
export default [
...global,
...player,
...salonpostam
] as Command[]

View File

@@ -0,0 +1,60 @@
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ChannelType } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { generateDiscoEmbed } from "@/utils/player"
import type { Disco } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("disco")
.setDescription("Manage the Disco module")
.setDescriptionLocalizations({ fr: "Gérer le module Disco" })
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand => subcommand
.setName("status")
.setDescription("Display Disco mode status")
.setNameLocalizations({ fr: "statut" })
.setDescriptionLocalizations({ fr: "Afficher le statut du mode Disco" })
)
.addSubcommand(subcommand => subcommand
.setName("channel")
.setDescription("Configure the channel for Disco effects")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Configurer le canal pour les effets Disco" })
.addChannelOption(option => option
.setName("channel")
.setDescription("The channel where to apply Disco effects")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Le canal où appliquer les effets Disco" })
.setRequired(true)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildPlayer.disco") as Disco
const subcommand = interaction.options.getSubcommand(true)
if (subcommand === "status") {
const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.reply({ embeds: [embed], components: components, flags: MessageFlags.Ephemeral })
}
else if (subcommand === "channel") {
const channel = interaction.options.getChannel("channel", true)
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) return interaction.reply({
content: t(interaction.locale, "common.invalid_channel_type"),
flags: MessageFlags.Ephemeral
})
dbData.channelId = channel.id
guildProfile.set("guildPlayer.disco", dbData)
guildProfile.markModified("guildPlayer.disco")
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "player.disco.channel_configured_success", { channel: channel.name ?? "Inconnu" }), flags: MessageFlags.Ephemeral })
}
}

View File

@@ -0,0 +1,31 @@
import * as disco from "./disco"
import * as loop from "./loop"
import * as lyrics from "./lyrics"
import * as panel from "./panel"
import * as pause from "./pause"
import * as play from "./play"
import * as previous from "./previous"
import * as queue from "./queue"
import * as resume from "./resume"
import * as shuffle from "./shuffle"
import * as skip from "./skip"
import * as stop from "./stop"
import * as volume from "./volume"
import type { Command } from "@/types"
export default [
disco,
loop,
lyrics,
panel,
pause,
play,
previous,
queue,
resume,
shuffle,
skip,
stop,
volume
] as Command[]

View File

@@ -1,21 +1,29 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'
import { useQueue } from'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('loop')
.setDescription('Boucler la musique en cours de lecture.')
.addIntegerOption(option => option.setName('loop')
.setDescription('Mode de boucle (0 = Off, 1 = Titre, 2 = File d\'Attente; 3 = Autoplay)')
.setRequired(true)
.setMinValue(0)
.setMaxValue(3)),
async execute(interaction: ChatInputCommandInteraction) {
let loop = interaction.options.getInteger('loop')
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.setRepeatMode(loop as number)
return await interaction.reply(`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`)
}
}
import { SlashCommandBuilder } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import type { QueueRepeatMode } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("loop")
.setDescription("Loop the current music")
.setNameLocalizations({ fr: "boucle" })
.setDescriptionLocalizations({ fr: "Boucler la musique en cours de lecture" })
.addIntegerOption(option => option
.setName("mode")
.setDescription("Loop mode (0 = Off | 1 = Track | 2 = Queue | 3 = Autoplay)")
.setDescriptionLocalizations({ fr: "Mode de boucle (0 = Arrêt | 1 = Titre | 2 = File d'Attente | 3 = Autoplay)" })
.setRequired(true)
.setMinValue(0)
.setMaxValue(3)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const mode = interaction.options.getInteger("mode", true)
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue") })
queue.setRepeatMode(mode as QueueRepeatMode)
return interaction.reply(t(interaction.locale, mode === 0 ? "player.loop_off" : mode === 1 ? "player.loop_track" : mode === 2 ? "player.loop_queue" : "player.loop_autoplay"))
}

View File

@@ -1,45 +1,61 @@
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js'
import { useQueue } from 'discord-player'
import { lyricsExtractor } from '@discord-player/extractor'
export default {
data: new SlashCommandBuilder()
.setName('lyrics')
.setDescription('Rechercher les paroles d\'une musique.')
.addStringOption(option => option.setName('recherche').setDescription('Chercher une musique spécifique')),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply()
let query = interaction.options.getString('recherche', false)
if (!query) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
let track = queue.currentTrack
if (!track) return interaction.followUp({ content: 'Aucune musique en cours, recherche en une plutôt !' })
if (track.raw.source === 'spotify') query = `${track.author} ${track.title}`
else query = track.title
}
let lyricsFinder = lyricsExtractor()
let lyrics = await lyricsFinder.search(query).catch(() => null)
if (!lyrics) return interaction.followUp({ content: 'Pas de paroles trouvées !' })
let trimmedLyrics = lyrics.lyrics.substring(0, 1997)
let embed = new EmbedBuilder()
.setColor('#ffc370')
.setTitle(lyrics.title)
.setURL(lyrics.url)
.setThumbnail(lyrics.thumbnail)
.setAuthor({
name: lyrics.artist.name,
iconURL: lyrics.artist.image,
url: lyrics.artist.url
})
.setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics)
return interaction.followUp({ embeds: [embed] })
}
}
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue, useMainPlayer } from "discord-player"
import type { LrcSearchResult } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("lyrics")
.setDescription("Search for song lyrics")
.setNameLocalizations({ fr: "paroles" })
.setDescriptionLocalizations({ fr: "Rechercher les paroles d'une musique" })
.addStringOption(option => option
.setName("search")
.setDescription("Search for a specific song")
.setNameLocalizations({ fr: "recherche" })
.setDescriptionLocalizations({ fr: "Chercher une musique spécifique" })
)
export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply()
const player = useMainPlayer()
const embed = new EmbedBuilder().setColor("#ffff64").setFooter({ text: "Powered by Genius" })
let lyrics = [] as LrcSearchResult[]
const query = interaction.options.getString("search", false)
if (!query) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue"), flags: MessageFlags.Ephemeral })
const track = queue.currentTrack
if (!track) return interaction.followUp({ content: t(interaction.locale, "player.no_current_track"), flags: MessageFlags.Ephemeral })
lyrics = await player.lyrics.search({ trackName: track.title, artistName: track.author })
if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral })
const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997)
embed
.setTitle(track.title)
.setURL(track.url)
.setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics)
.setThumbnail(track.thumbnail)
.setAuthor({ name: track.author, url: `https://genius.com/search?q=${track.author.replace(/ /g, "-")}` })
}
else {
lyrics = await player.lyrics.search({ q: query })
if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral })
const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997)
embed
.setTitle(lyrics[0].name)
.setURL(`https://genius.com/search?q=${query.replace(/ /g, "%20")}`)
.setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics)
.setThumbnail("https://play-lh.googleusercontent.com/e6-dZlTM-gJ2sFxFFs3X15O84HEv6jc9PQGgHtVTn7FP6lUXeEAkDl9v4RfVOwbSuQ")
.setAuthor({ name: lyrics[0].artistName, url: `https://genius.com/search?q=${lyrics[0].artistName.replace(/ /g, "-")}` })
}
return interaction.followUp({ embeds: [embed] })
}

View File

@@ -1,25 +1,26 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { playerGenerate } from '../../utils/player'
import getUptime from '../../utils/getUptime'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('panel')
.setDescription('Générer les infos de la lecture en cours.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
let guild = interaction.guild
if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true })
let client = guild.client
let { embed, components } = await playerGenerate(guild)
if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)} \n ${embed.data.footer.text}` })
else embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)}` })
return interaction.reply({ embeds: [embed] })
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { generatePlayerEmbed } from "@/utils/player"
import uptime from "@/utils/uptime"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("panel")
.setDescription("Generate current playback info")
.setNameLocalizations({ fr: "panneau" })
.setDescriptionLocalizations({ fr: "Générer les infos de la lecture en cours" })
export async function execute(interaction: ChatInputCommandInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue"), flags: MessageFlags.Ephemeral })
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "common.private_message_not_available"), flags: MessageFlags.Ephemeral })
const { embed, components } = generatePlayerEmbed(guild, interaction.locale)
if (components && embed.data.footer) embed.setFooter({ text: `${t(interaction.locale, "player.uptime")}: ${uptime(guild.client.uptime)} \n ${embed.data.footer.text}` })
else embed.setFooter({ text: `${t(interaction.locale, "player.uptime")}: ${uptime(guild.client.uptime)}` })
return interaction.reply({ embeds: [embed] })
}

View File

@@ -1,15 +1,17 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('pause')
.setDescription('Met en pause la musique.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.node.setPaused(!queue.node.isPaused())
return await interaction.reply('Musique mise en pause !')
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("pause")
.setDescription("Pause the music")
.setDescriptionLocalizations({ fr: "Met en pause la musique" })
export const execute = async (interaction: ChatInputCommandInteraction) => {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral })
queue.node.setPaused(!queue.node.isPaused())
return interaction.reply(t(interaction.locale, "player.paused"))
}

View File

@@ -1,96 +1,123 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from 'discord.js'
import { useMainPlayer, useQueue, QueryType } from 'discord-player'
import dbGuild from '../../schemas/guild'
interface TrackSearchResult { name: string, value: string }
export default {
data: new SlashCommandBuilder()
.setName('play')
.setDescription('Jouer une musique.')
.addStringOption(option => option.setName('recherche').setDescription('Titre de la musique à chercher').setRequired(true).setAutocomplete(true)),
async autocompleteRun(interaction: AutocompleteInteraction) {
let query = interaction.options.getString('recherche', true)
if (!query) return interaction.respond([])
let player = useMainPlayer()
const resultsYouTube = await player.search(query, { searchEngine: QueryType.YOUTUBE })
const resultsSpotify = await player.search(query, { searchEngine: QueryType.SPOTIFY_SEARCH })
const tracksYouTube = resultsYouTube.tracks.slice(0, 5).map((t) => ({
name: `YouTube: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`,
value: t.url
}))
const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map((t) => ({
name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`,
value: t.url
}))
const tracks: TrackSearchResult[] = []
tracksYouTube.forEach((t) => tracks.push({ name: t.name, value: t.value }))
tracksSpotify.forEach((t) => tracks.push({ name: t.name, value: t.value }))
return interaction.respond(tracks)
},
async execute(interaction: ChatInputCommandInteraction) {
let member = interaction.member as GuildMember
let voiceChannel = member.voice.channel
if (!voiceChannel) return await interaction.reply({ content: 'T\'es pas dans un vocal, idiot !', ephemeral: true })
let botChannel = interaction.guild?.members.me?.voice.channel
if (botChannel && voiceChannel.id !== botChannel.id) return await interaction.reply({ content: 'T\'es pas dans mon vocal !', ephemeral: true })
await interaction.deferReply()
let query = interaction.options.getString('recherche', true)
let player = useMainPlayer()
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) {
if (interaction.guild) queue = player.nodes.create(interaction.guild, {
metadata: {
channel: interaction.channel,
client: interaction.guild.members.me,
requestedBy: interaction.user
},
selfDeaf: true,
volume: 20,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 30000,
leaveOnEnd: true,
leaveOnEndCooldown: 300000
})
else return
}
try { if (!queue.connection) await queue.connect(voiceChannel) }
catch (error: unknown) { console.error(error) }
let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id })
if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`)
let dbData = guildProfile.get('guildPlayer.replay')
dbData['textChannelId'] = interaction.channel?.id
dbData['voiceChannelId'] = voiceChannel.id
guildProfile.set('guildPlayer.replay', dbData)
await guildProfile.save().catch(console.error)
let result = await player.search(query, { requestedBy: interaction.user })
if (!result.hasTracks()) return interaction.followUp(`Aucune musique trouvée pour **${query}** !`)
let track = result.tracks[0]
let entry = queue.tasksQueue.acquire()
await entry.getTask()
queue.addTrack(track)
try {
if (!queue.isPlaying()) await queue.node.play()
let track_source = track.source === 'youtube' ? 'Youtube' : track.source === 'spotify' ? 'Spotify' : 'Inconnu'
return interaction.followUp(`Chargement de la musique **${track.title}** de **${track.author}** sur **${track_source}**...`)
} catch (error: unknown) { console.error(error) }
finally { queue.tasksQueue.release() }
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from "discord.js"
import { useMainPlayer, useQueue } from "discord-player"
import { SpotifyExtractor } from "@discord-player/extractor"
import { YoutubeiExtractor } from "discord-player-youtubei"
import { startProgressSaving } from "@/utils/player"
import type { TrackSearchResult } from "@/types/player"
import type { GuildPlayer } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("play")
.setDescription("Play a song")
.setNameLocalizations({ fr: "jouer" })
.setDescriptionLocalizations({ fr: "Jouer une musique" })
.addStringOption(option => option
.setName("search")
.setDescription("Music title to search for")
.setNameLocalizations({ fr: "recherche" })
.setDescriptionLocalizations({ fr: "Titre de la musique à chercher" })
.setRequired(true)
.setAutocomplete(true)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const member = interaction.member as GuildMember
const voiceChannel = member.voice.channel
if (!voiceChannel) return interaction.reply({ content: t(interaction.locale, "player.not_in_voice"), flags: MessageFlags.Ephemeral })
const botChannel = interaction.guild?.members.me?.voice.channel
if (botChannel && voiceChannel.id !== botChannel.id) return interaction.reply({ content: t(interaction.locale, "player.not_in_same_voice"), flags: MessageFlags.Ephemeral })
await interaction.deferReply()
const query = interaction.options.getString("search", true)
const player = useMainPlayer()
let queue = useQueue(interaction.guild?.id ?? "")
if (!queue) {
if (interaction.guild) queue = player.nodes.create(interaction.guild, {
metadata: {
channel: interaction.channel,
client: interaction.guild.members.me,
requestedBy: interaction.user
},
selfDeaf: true,
volume: 20,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 30000,
leaveOnEnd: true,
leaveOnEndCooldown: 300000
})
else return
}
try { if (!queue.connection) await queue.connect(voiceChannel) }
catch (error) { console.error(error) }
const guildProfile = await dbGuild.findOne({ guildId: queue.guild.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const botId = interaction.client.user.id
const dbData = guildProfile.get("guildPlayer") as GuildPlayer
dbData.instances ??= []
const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId)
const instance = { botId, replay: {
textChannelId: interaction.channel?.id ?? "",
voiceChannelId: voiceChannel.id,
trackUrl: "",
progress: 0
} }
if (instanceIndex === -1) dbData.instances.push(instance)
else dbData.instances[instanceIndex] = instance
guildProfile.set("guildPlayer", dbData)
guildProfile.markModified("guildPlayer")
await guildProfile.save().catch(console.error)
const result = await player.search(query, { requestedBy: interaction.user })
if (!result.hasTracks()) return interaction.followUp({ content: t(interaction.locale, "player.no_track_found", { query }), flags: MessageFlags.Ephemeral })
const track = result.tracks[0]
const entry = queue.tasksQueue.acquire()
await entry.getTask()
queue.addTrack(track)
try {
if (!queue.isPlaying()) await queue.node.play()
startProgressSaving(queue.guild.id, botId)
const track_source = track.source === "spotify" ? t(interaction.locale, "player.sources.spotify") : track.source === "youtube" ? t(interaction.locale, "player.sources.youtube") : t(interaction.locale, "player.sources.unknown")
return await interaction.followUp(t(interaction.locale, "player.loading_track", { title: track.title, author: track.author, source: track_source }))
}
catch (error) { console.error(error) }
finally { queue.tasksQueue.release() }
}
export async function autocompleteRun(interaction: AutocompleteInteraction) {
const query = interaction.options.getString("search", true)
if (!query) return interaction.respond([])
const player = useMainPlayer()
const resultsSpotify = await player.search(query, { searchEngine: `ext:${SpotifyExtractor.identifier}` })
const resultsYouTube = await player.search(query, { searchEngine: `ext:${YoutubeiExtractor.identifier}` })
const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map(t => ({
name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`,
value: t.url
}))
const tracksYouTube = resultsYouTube.tracks.slice(0, 5).map(t => ({
name: `YouTube: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`,
value: t.url
}))
const tracks: TrackSearchResult[] = []
tracksSpotify.forEach((t) => tracks.push({ name: t.name, value: t.value }))
tracksYouTube.forEach((t) => tracks.push({ name: t.name, value: t.value }))
return interaction.respond(tracks)
}

View File

@@ -1,15 +1,18 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useHistory } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('previous')
.setDescription('Joue la musique précédente.'),
async execute(interaction: ChatInputCommandInteraction) {
let history = useHistory(interaction.guild?.id ?? '')
if (!history) return await interaction.reply('Il n\'y a pas d\'historique de musique !')
await history.previous()
return await interaction.reply('Musique précédente jouée !')
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useHistory } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("previous")
.setDescription("Play the previous song")
.setNameLocalizations({ fr: "precedent" })
.setDescriptionLocalizations({ fr: "Joue la musique précédente" })
export async function execute(interaction: ChatInputCommandInteraction) {
const history = useHistory(interaction.guild?.id ?? "")
if (!history) return interaction.reply({ content: t(interaction.locale, "player.no_session"), flags: MessageFlags.Ephemeral })
await history.previous()
return interaction.reply({ content: t(interaction.locale, "player.previous_played"), flags: MessageFlags.Ephemeral })
}

View File

@@ -1,19 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('queue')
.setDescription("Récupérer la file d'attente."),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.reply({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
if (!queue.currentTrack) return interaction.reply({ content: 'Aucune musique en cours de lecture.' })
let track = `[${queue.currentTrack.title}](${queue.currentTrack.url})`
let tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` })
if (tracks.length === 0) return interaction.reply({ content: `Lecture en cours : ${track} \nAucune musique dans la file d'attente.` })
return interaction.reply({ content: `Lecture en cours : ${track} \nFile d'attente actuelle : \n${tracks.join('\n')}` })
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("queue")
.setDescription("Get the queue")
.setNameLocalizations({ fr: "file" })
.setDescriptionLocalizations({ fr: "Récupérer la file d'attente." })
export async function execute(interaction: ChatInputCommandInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.reply({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
if (!queue.currentTrack) return interaction.reply({ content: t(interaction.locale, "player.no_track_playing"), flags: MessageFlags.Ephemeral})
const track = `[${queue.currentTrack.title}](${queue.currentTrack.url})`
const tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` })
if (tracks.length === 0) return interaction.reply({ content: t(interaction.locale, "player.now_playing_no_queue", { track }) })
return interaction.reply({ content: t(interaction.locale, "player.now_playing_with_queue", { track, tracks: tracks.join("\n") }) })
}

View File

@@ -1,15 +1,18 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('resume')
.setDescription('Reprendre la musique.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.node.setPaused(!queue.node.isPaused())
return await interaction.reply('Musique reprise !')
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("resume")
.setDescription("Resume the music")
.setNameLocalizations({ fr: "reprendre" })
.setDescriptionLocalizations({ fr: "Reprendre la musique" })
export async function execute(interaction: ChatInputCommandInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
queue.node.setPaused(!queue.node.isPaused())
return interaction.reply(t(interaction.locale, "player.resumed"))
}

View File

@@ -1,15 +1,18 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('shuffle')
.setDescription('Mélange la file d\'attente.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.tracks.shuffle()
return await interaction.reply('File d\'attente mélangée !')
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("shuffle")
.setDescription("Shuffle the queue")
.setNameLocalizations({ fr: "melanger" })
.setDescriptionLocalizations({ fr: "Mélange la file d'attente" })
export async function execute(interaction: ChatInputCommandInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
queue.tracks.shuffle()
return interaction.reply(t(interaction.locale, "player.shuffled"))
}

View File

@@ -1,15 +1,18 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('skip')
.setDescription('Passer la musique en cours.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.node.skip()
return await interaction.reply('Musique passée !')
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("skip")
.setDescription("Skip the current song")
.setNameLocalizations({ fr: "passer" })
.setDescriptionLocalizations({ fr: "Passer la musique en cours" })
export async function execute(interaction: ChatInputCommandInteraction) {
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
queue.node.skip()
return interaction.reply(t(interaction.locale, "player.skipped"))
}

View File

@@ -1,15 +1,21 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('stop')
.setDescription('Arrêter la musique.'),
async execute(interaction: ChatInputCommandInteraction) {
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.delete()
return await interaction.reply('Musique arrêtée !')
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("stop")
.setDescription("Stop the music")
.setNameLocalizations({ fr: "arreter" })
.setDescriptionLocalizations({ fr: "Arrêter la musique" })
export async function execute(interaction: ChatInputCommandInteraction) {
await stopProgressSaving(interaction.guild?.id ?? "", interaction.client.user.id)
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
queue.delete()
return interaction.reply(t(interaction.locale, "player.stopped"))
}

View File

@@ -1,21 +1,26 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
import { useQueue } from 'discord-player'
export default {
data: new SlashCommandBuilder()
.setName('volume')
.setDescription('Modifie le volume de la musique.')
.addIntegerOption(option => option.setName('volume')
.setDescription('Le volume à mettre (%)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100)),
async execute(interaction: ChatInputCommandInteraction) {
let volume = interaction.options.getInteger('volume')
let queue = useQueue(interaction.guild?.id ?? '')
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' })
queue.node.setVolume(volume as number)
return await interaction.reply(`Volume modifié à ${volume}% !`)
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("volume")
.setDescription("Change the music volume")
.setDescriptionLocalizations({ fr: "Modifie le volume de la musique" })
.addIntegerOption(option => option
.setName("volume")
.setDescription("The volume to set (%)")
.setDescriptionLocalizations({ fr: "Le volume à mettre (%)" })
.setRequired(true)
.setMinValue(0)
.setMaxValue(100)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const volume = interaction.options.getInteger("volume", true)
const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral})
queue.node.setVolume(volume)
return interaction.reply(t(interaction.locale, "player.volume_changed", { volume: volume.toString() }))
}

View File

@@ -1,58 +1,70 @@
import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, MessageReaction, User }from 'discord.js'
import * as crack from '../../utils/crack'
export default {
data: new SlashCommandBuilder().setName('crack').setDescription('Télécharge un crack sur le site online-fix.me !')
.addStringOption(option => option.setName('jeu').setDescription('Quel jeu tu veux DL ?').setRequired(true)),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply()
let query = interaction.options.getString('jeu')
if (!query) return
let games = await crack.search(query) as crack.Game[]
if (!Array.isArray(games)) {
//if (games.toString() == "TypeError: Cannot read properties of undefined (reading 'split')") return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` })
//else return interaction.followUp({ content: "Une erreur s'est produite ! ```" + games + "```" })
return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` })
}
let game = {} as crack.Game
if (games.length > 1) {
games = games.slice(0, 9)
let list = ''
for (let i = 0; i < games.length; i++) list += `\n${i + 1}. ${games[i].name} (${games[i].link})`
let message = await interaction.followUp({ content: `J'ai trouvé plusieurs jeux pour "${query}" ! ${list}` })
let emojis = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣']
for (let i = 0; i < games.length; i++) await message.react(emojis[i])
// Wait for a reaction to be added by the interaction author.
const filter = (reaction: MessageReaction, user: User) => { if (reaction.emoji.name) { return emojis.includes(reaction.emoji.name) && user.id === interaction.user.id } return false }
await message.awaitReactions({ filter, max: 1, time: 5000, errors: ['time'] }).then(collected => {
console.log(collected)
if (!collected.first) return
let reaction = collected.first()
let index = emojis.indexOf(reaction?.emoji.name ?? '')
game = games[index]
}).catch(() => { return interaction.followUp({ content: "T'as mis trop de temps à choisir !" }) })
}
else game = games[0]
let url = await crack.repo(game)
if (!url) return
let file = await crack.torrent(url)
if (!file) return
let filePath = await crack.download(url, file)
if (!filePath) return
let link = await crack.magnet(filePath)
let embed = new EmbedBuilder()
.setColor('#ffc370')
.setTitle(game.name)
.setURL(game.link)
.setDescription(`Voici ce que j'ai trouvé pour "${query}".\nTu peux aussi cliquer sur [ce lien](https://angels-dev.fr/magnet/${link}) pour pouvoir télécharger le jeu direct !`)
await interaction.followUp({ embeds: [embed], files: [filePath] })
}
}
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, MessageReaction, User } from "discord.js"
import { search, repo, torrent, download, magnet } from "@/utils/crack"
import type { CrackGame } from "@/types"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("crack")
.setDescription("Download a crack from online-fix.me")
.setDescriptionLocalizations({ fr: "Télécharge un crack sur online-fix.me" })
.addStringOption(option => option
.setName("game")
.setDescription("What game do you want to download?")
.setNameLocalizations({ fr: "jeu" })
.setDescriptionLocalizations({ fr: "Quel jeu tu veux télécharger ?" })
.setRequired(true)
)
export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply()
const query = interaction.options.getString("game", true)
let games = await search(query)
if (!Array.isArray(games)) return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.no_games_found", { query }), flags: MessageFlags.Ephemeral })
let game = {} as CrackGame
if (games.length > 1) {
games = games.slice(0, 9)
let list = ""
for (let i = 0; i < games.length; i++) list += `\n${i + 1}. ${games[i].name} (${games[i].link})`
const message = await interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.multiple_games_found", { query, list }) })
const emojis = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣"]
for (let i = 0; i < games.length; i++) await message.react(emojis[i])
// Wait for a reaction to be added by the interaction author.
const filter = (reaction: MessageReaction, user: User) => {
if (reaction.emoji.name) return (emojis.includes(reaction.emoji.name) && user.id === interaction.user.id)
return false
}
await message.awaitReactions({ filter, max: 1, time: 5000, errors: ["time"] }).then(collected => {
console.log(collected)
const reaction = collected.first()
const index = emojis.indexOf(reaction?.emoji.name ?? "")
if (!games) return
game = games[index]
})
.catch(() => { return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.selection_timeout"), flags: MessageFlags.Ephemeral }) })
} else game = games[0]
const url = await repo(game)
if (!url) return
const file = await torrent(url)
if (!file) return
const filePath = await download(url, file)
if (!filePath) return
const link = magnet(filePath)
const embed = new EmbedBuilder()
.setColor("#ffc370")
.setTitle(game.name)
.setURL(game.link)
.setDescription(t(interaction.locale, "salonpostam.crack.game_found", { query, link: `https://angels-dev.fr/magnet/${link}` }))
await interaction.followUp({ embeds: [embed], files: [filePath] })
}

View File

@@ -1,151 +0,0 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, Message, inlineCode } from 'discord.js'
import * as Freebox from '../../utils/freebox'
import dbGuild from '../../schemas/guild'
import crypto from 'crypto'
import https from 'https'
//import path from 'path'
//import fs from 'fs'
interface ReturnMsgData {
status: string
error_code?: string
Title?: string
Message?: string
}
function returnMsg(result: ReturnMsgData) {
if (result.status === 'fail') return `La commande a échouée !\n${inlineCode(`${result.Title}: ${result.Message}`)}`
if (result.status === 'error') return `Y'a eu une erreur !\n${inlineCode(`${result.error_code}`)}`
}
export default {
data: new SlashCommandBuilder().setName('freebox').setDescription("Accéder à l'API FreeboxOS !")
.addSubcommand(subcommand => subcommand.setName('import').setDescription("Envoyer un fichier d'autorité de certification."))
.addSubcommand(subcommand => subcommand.setName('version').setDescription("Afficher la version de l'API."))
.addSubcommand(subcommand => subcommand.setName('init').setDescription("Créer une app sur la Freebox pour s'authentifier."))
.addSubcommandGroup(subcommandGroup => subcommandGroup.setName('get').setDescription('Récupérer des données.')
.addSubcommand(subcommand => subcommand.setName('connection').setDescription('Récupérer les informations de connexion.'))
),
async execute(interaction: ChatInputCommandInteraction) {
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id })
if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` })
let dbData = guildProfile.get('guildFbx')
if (!dbData?.enabled) return interaction.reply({ content: `Freebox module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildFbx.enabled True\` !` })
let host = dbData.host as string
if (!host) return interaction.reply({ content: `Freebox host is not set for **${interaction.guild?.name}**, please set with \`/database edit guildFbx.host <host>\` !` })
let version = dbData.version as number
if (!version) return interaction.reply({ content: `Freebox API version is not set for **${interaction.guild?.name}**, please set with \`/database edit guildFbx.version <version>\` !` })
let httpsOptions = {}
//let caCrt = fs.readFileSync(path.resolve(__dirname, '../../static/freebox-ecc-root-ca.crt'))
// MIME Type : application/x-x509-ca-cert
//if (caCrt) httpsOptions = { ca: caCrt }
let httpsAgent = new https.Agent(httpsOptions)
if (interaction.options.getSubcommand() === 'import') {
let filter = (m: Message) => m.author.id === interaction.user.id
await interaction.reply({ content: 'Please send another message with the CA file attached, you have one minute.', fetchReply: true }).then(async () => {
console.log('waiting for message')
await interaction.channel?.awaitMessages({ filter, time: 60_000, errors: ['time'] }).then(async collected => {
console.log(collected)
let message = collected.first()
if (!message?.attachments.size) return interaction.followUp('No file was sent in your message!')
let attachment = message.attachments.first()
console.log(attachment)
// Save the file to the database // TODO
interaction.followUp(`File saved, you can now interact with your Freebox!`)
}).catch(() => interaction.followUp('No message was sent before the time limit!'))
})
}
else if (interaction.options.getSubcommand() === 'version') {
let result = await Freebox.Core.Version(host, httpsAgent)
if (result.status === 'success') {
let embed = new EmbedBuilder()
embed.setTitle('FreeboxOS API Version')
embed.setDescription(`Version: ${result.data.api_version}`)
return await interaction.reply({ embeds: [embed] })
}
else if (result.status === 'fail') return await interaction.reply({ content: `Failed to retrieve the API version: ${result.data}`, ephemeral: true })
else if (result.status === 'error') return await interaction.reply({ content: `An error occurred while retrieving the API version: ${result.data}`, ephemeral: true })
}
else if (interaction.options.getSubcommand() === 'init') {
await interaction.deferReply({ ephemeral: true })
let app = {
app_id: 'fr.angels.bot_tamiseur',
app_name: 'Bot Tamiseur',
app_version: '2.3.0',
device_name: 'Bot Discord NodeJS'
}
let result = await Freebox.Core.Init(host, version, httpsAgent, app, '')
if (result.status === 'success') {
let appToken = result.data.app_token
let trackId = result.data.track_id
let initCheck = setInterval(async () => {
let result = await Freebox.Core.Init(host, version, httpsAgent, app, trackId)
if (result.status !== 'success') return await interaction.followUp(returnMsg(result) as string)
let status = result.data.status
if (status === 'granted') {
clearInterval(initCheck)
let password_salt = result.data.password_salt
if (!dbData) return
dbData['appToken'] = appToken
dbData['password_salt'] = password_salt
if (!guildProfile) return
guildProfile.set('guildFbx', dbData)
guildProfile.markModified('guildFbx')
await guildProfile.save().catch(console.error)
return await interaction.followUp('Done !')
}
else if (status === 'denied') {
clearInterval(initCheck)
return await interaction.followUp('The user denied the app access to the Freebox.')
}
else if (status === 'pending') return
}, 2000)
} else return await interaction.followUp({ content: returnMsg(result) as string, ephemeral: true })
}
else if (interaction.options.getSubcommandGroup() === 'get') {
let appToken = dbData.appToken as string
if (!appToken) return await interaction.reply({ content: `Freebox appToken is not set for **${interaction.guild?.name}**, please init the app with \`/freebox init\` !` })
console.log(appToken)
let challengeData = await Freebox.Login.Challenge(host, version, httpsAgent)
if (!challengeData) return await interaction.reply({ content: `Failed to retrieve the challenge for **${interaction.guild?.name}** !` })
let challenge = challengeData.data.challenge
console.log(challenge)
let password = crypto.createHmac('sha1', appToken).update(challenge).digest('hex')
console.log(password)
let session = await Freebox.Login.Session(host, version, httpsAgent, 'fr.angels.bot_tamiseur', password)
if (!session) return await interaction.reply({ content: `Failed to retrieve the session for **${interaction.guild?.name}** !` })
let sessionToken = dbData['sessionToken'] = session.data.session_token
guildProfile.set('guildFbx', dbData)
guildProfile.markModified('guildFbx')
await guildProfile.save().catch(console.error)
if (interaction.options.getSubcommand() === 'connection') {
let connection = await Freebox.Get.Connection(host, version, httpsAgent, sessionToken)
if (!connection) return await interaction.reply({ content: `Failed to retrieve the connection details for **${interaction.guild?.name}** !` })
return await interaction.reply({ content: `Connection details for **${interaction.guild?.name}**:\n${inlineCode(JSON.stringify(connection))}` })
}
}
}
}

View File

@@ -0,0 +1,15 @@
import * as crack from "./crack"
import * as papa from "./papa"
import * as parle from "./parle"
import * as spam from "./spam"
import * as update from "./update"
import type { Command } from "@/types"
export default [
crack,
papa,
parle,
spam,
update
] as Command[]

View File

@@ -1,34 +1,34 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js'
import { getVoiceConnection, joinVoiceChannel } from '@discordjs/voice'
export default {
data: new SlashCommandBuilder()
.setName('papa')
.setDescription('Si papa m\'appelle, je le rejoins !'),
async execute(interaction: ChatInputCommandInteraction) {
if (interaction.user.id !== '223831938346123275') return interaction.reply({ content: 'T\'es pas mon père, dégage !' })
let guild = interaction.guild
if (!guild) return interaction.reply({ content: 'Je ne peux pas rejoindre ton vocal en message privé, papa !' })
let member = interaction.member as GuildMember
let botChannel = guild.members.me?.voice.channel
let papaChannel = member.voice.channel
if (!papaChannel && botChannel) {
const voiceConnection = getVoiceConnection(guild.id);
if (voiceConnection) voiceConnection.destroy()
return interaction.reply({ content: 'Je quitte le vocal, papa !' })
}
else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) {
joinVoiceChannel({
channelId: papaChannel.id,
guildId: papaChannel.guild.id,
adapterCreator: papaChannel.guild.voiceAdapterCreator,
})
return interaction.reply({ content: 'Je rejoins ton vocal, papa !' })
}
else return interaction.reply({ content: 'Je suis déjà dans ton vocal, papa !' })
}
}
import { SlashCommandBuilder } from "discord.js"
import type { ChatInputCommandInteraction, GuildMember } from "discord.js"
import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("papa")
.setDescription("If daddy calls me, I join him")
.setDescriptionLocalizations({ fr: "Si papa m'appelle, je le rejoins" })
export async function execute(interaction: ChatInputCommandInteraction) {
if (interaction.user.id !== "223831938346123275") return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.not_your_father") })
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.no_dm") })
const member = interaction.member as GuildMember
const botChannel = guild.members.me?.voice.channel
const papaChannel = member.voice.channel
if (!papaChannel && botChannel) {
const voiceConnection = getVoiceConnection(guild.id)
if (voiceConnection) voiceConnection.destroy()
return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.leaving_voice") })
} else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) {
joinVoiceChannel({
channelId: papaChannel.id,
guildId: papaChannel.guild.id,
adapterCreator: papaChannel.guild.voiceAdapterCreator,
})
return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.joining_voice") })
} else return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.already_connected") })
}

View File

@@ -1,79 +1,84 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js'
import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from '@discordjs/voice'
export default {
data: new SlashCommandBuilder()
.setName('parle')
.setDescription('Fais moi parler par dessus quelqu\'un de chiant dans le vocal')
.addUserOption(option => option.setName('user').setDescription('La personne en question').setRequired(true)),
async execute(interaction: ChatInputCommandInteraction) {
if (interaction.user.id !== '223831938346123275') return await interaction.reply({ content: 'Tu n\'as pas le droit d\'utiliser cette commande !', ephemeral: true })
let user = interaction.options.getUser('user')
if (!user) return
let guild = interaction.guild
if (!guild) return
let member = guild.members.cache.get(user.id) as GuildMember
if (!member) return
let caller = interaction.member as GuildMember
if (!caller) return
if (!caller.voice.channel) return await interaction.reply({ content: 'You must be in a voice channel to use this command.', ephemeral: true })
if (!member.voice.channel) return await interaction.reply({ content: 'The member must be in a voice channel to use this command.', ephemeral: true })
if (caller.voice.channelId !== member.voice.channelId) return await interaction.reply({ content: 'You must be in the same voice channel than the member to use this command.', ephemeral: true })
await interaction.reply({ content: 'Je vais parler par dessus cette personne !', ephemeral: true })
/*
// Searches for audio files uploaded in the channel
let messages = await interaction.channel.messages.fetch({ limit: 10, cache: false })
messages = messages.filter(m => m.attachments.size > 0)
let files = []
await messages.forEach(m => m.attachments.forEach(a => {
if (a.contentType === 'audio/mpeg') files.push(a)
}))
if (files.size === 0) return await interaction.editReply({ content: 'Aucun fichier audio trouvé dans ce channel.', ephemeral: true })
// Limit the number of files to the last 10
//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10)
// Ask the user to choose a file
let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files: files })
let filter = m => m.author.id === interaction.user.id && !isNaN(m.content) && parseInt(m.content) > 0 && parseInt(m.content) <= files.size
let response = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] })
file = files.get(files.keyArray()[response.first().content - 1])
*/
let playing = false
let player = createAudioPlayer()
player.on(AudioPlayerStatus.Idle, () => { playing = false })
let connection = joinVoiceChannel({
channelId: caller.voice.channelId as string,
guildId: interaction.guildId as string,
adapterCreator: guild.voiceAdapterCreator,
selfDeaf: false
})
connection.subscribe(player)
let stream = connection.receiver.subscribe(user.id, { end: { behavior: EndBehaviorType.Manual } })
stream.on('data', () => {
if (!user) return
if (connection.receiver.speaking.users.has(user.id) && !playing) {
playing = true
let resource = createAudioResource('../../static/parle.mp3', { inlineVolume: true })
//let resource = createAudioResource(file.attachments.first().url, { inlineVolume: true })
if (resource.volume) resource.volume.setVolume(0.2)
player.play(resource)
}
})
interaction.client.on('voiceStateUpdate', (oldState, newState) => {
if (oldState.id === member.id && newState.channelId !== caller.voice.channelId) {
stream.destroy()
connection.disconnect()
}
})
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, GuildMember } from "discord.js"
import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from "@discordjs/voice"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("speak")
.setDescription("Make me talk over someone annoying in voice chat")
.setNameLocalizations({ fr: "parle" })
.setDescriptionLocalizations({ fr: "Fais moi parler par dessus quelqu'un d'ennuyant dans le vocal" })
.addUserOption(option => option
.setName("user")
.setDescription("The person in question")
.setNameLocalizations({ fr: "utilisateur" })
.setDescriptionLocalizations({ fr: "La personne en question" })
.setRequired(true)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guild = interaction.guild
if (!guild) return
const user = interaction.options.getUser("user", true)
const member = await guild.members.fetch(user.id)
const caller = interaction.member as GuildMember
if (!caller.voice.channel) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.not_in_voice"), flags: MessageFlags.Ephemeral })
if (!member.voice.channel) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.member_not_in_voice"), flags: MessageFlags.Ephemeral })
if (caller.voice.channelId !== member.voice.channelId) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.not_same_channel"), flags: MessageFlags.Ephemeral })
await interaction.reply({ content: t(interaction.locale, "salonpostam.parle.will_speak_over"), flags: MessageFlags.Ephemeral })
/*
// Searches for audio files uploaded in the channel
const messages = await interaction.channel.messages.fetch({ limit: 10, cache: false }).filter(m => m.attachments.size > 0)
const files = []
await messages.forEach(m => m.attachments.forEach(a => {
if (a.contentType === 'audio/mpeg') files.push(a)
}))
if (files.size === 0) return interaction.editReply({ content: t(interaction.locale, "player.no_audio_found"), flags: MessageFlags.Ephemeral })
// Limit the number of files to the last 10
//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10)
// Ask the user to choose a file
let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files })
const filter = m => m.author.id === interaction.user.id && !isNaN(m.content) && parseInt(m.content) > 0 && parseInt(m.content) <= files.size
const response = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] })
file = files.get(files.keyArray()[response.first().content - 1])
*/
let playing = false
const player = createAudioPlayer()
player.on(AudioPlayerStatus.Idle, () => { playing = false })
const connection = joinVoiceChannel({
channelId: caller.voice.channelId ?? "",
guildId: interaction.guildId ?? "",
adapterCreator: guild.voiceAdapterCreator,
selfDeaf: false
})
connection.subscribe(player)
const stream = connection.receiver.subscribe(user.id, {
end: { behavior: EndBehaviorType.Manual }
})
stream.on("data", () => {
if (connection.receiver.speaking.users.has(user.id) && !playing) {
playing = true
const resource = createAudioResource("@/static/parle.mp3", { inlineVolume: true })
//const resource = createAudioResource(file.attachments.first().url, { inlineVolume: true })
if (resource.volume) resource.volume.setVolume(0.2)
player.play(resource)
}
})
interaction.client.on("voiceStateUpdate", (oldState, newState) => {
if (oldState.id === member.id && newState.channelId !== caller.voice.channelId ) {
stream.destroy()
connection.disconnect()
}
})
}

View File

@@ -1,29 +1,47 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
export default {
data: new SlashCommandBuilder()
.setName('spam')
.setDescription('Spam')
.addUserOption(option => option.setName('user').setDescription('Spam').setRequired(true))
.addStringOption(option => option.setName('string').setDescription('Spam').setRequired(true))
.addIntegerOption(option => option.setName('integer').setDescription('Spam').setRequired(true)),
async execute(interaction: ChatInputCommandInteraction) {
let user = interaction.options.getUser('user')
let string = interaction.options.getString('string')
let integer = interaction.options.getInteger('integer')
await interaction.reply({ content: 'Spam', ephemeral: true })
let i = 0
function myLoop() {
setTimeout(function () {
if (!user) return
if (!string) return
if (!integer) return
user.send(string).catch(error => console.error(error))
i++
if (i < integer) myLoop()
}, 1000)
}
myLoop()
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("spam")
.setDescription("Spam a user with a message")
.setDescriptionLocalizations({ fr: "Spammer un utilisateur avec un message" })
.addUserOption(option => option
.setName("user")
.setDescription("Target user")
.setNameLocalizations({ fr: "utilisateur" })
.setDescriptionLocalizations({ fr: "Utilisateur cible" })
.setRequired(true)
)
.addStringOption(option => option
.setName("message")
.setDescription("Message to spam")
.setDescriptionLocalizations({ fr: "Message à spammer" })
.setRequired(true)
)
.addIntegerOption(option => option
.setName("count")
.setDescription("Number of times to spam")
.setNameLocalizations({ fr: "nombre" })
.setDescriptionLocalizations({ fr: "Nombre de fois à spammer" })
.setRequired(true)
.setMinValue(1)
.setMaxValue(100)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const user = interaction.options.getUser("user", true)
const string = interaction.options.getString("message", true)
const integer = interaction.options.getInteger("count", true)
await interaction.reply({ content: t(interaction.locale, "salonpostam.spam.started"), flags: MessageFlags.Ephemeral })
let i = 0
function myLoop() {
setTimeout(() => {
user.send(string).catch(console.error)
i++
if (i < integer) myLoop()
}, 1000)
}
myLoop()
}

View File

@@ -1,19 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, Guild } from 'discord.js'
export default {
data: new SlashCommandBuilder()
.setName('update')
.setDescription('Update the member count channel.'),
async execute(interaction: ChatInputCommandInteraction) {
let guild = interaction.guild as Guild
guild.members.fetch().then(() => {
let i = 0
guild.members.cache.forEach(async member => { if (!member.user.bot) i++ })
let channel = guild.channels.cache.get('1091140609139560508')
if (!channel) return
channel.setName(`${i} Gens Posés`)
interaction.reply(`${i} Gens Posés !`)
}).catch(console.error)
}
}
import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("update")
.setDescription("Update the member count channel")
.setDescriptionLocalizations({ fr: "Mettre à jour le canal de nombre de membres" })
export async function execute(interaction: ChatInputCommandInteraction) {
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "common.command_server_only"), flags: MessageFlags.Ephemeral })
guild.members.fetch().then(async () => {
let i = 0
guild.members.cache.forEach(member => { if (!member.user.bot) i++ })
const channel = guild.channels.cache.get("1091140609139560508")
if (!channel) return
await channel.setName(`${i} Gens Posés`)
return interaction.reply(t(interaction.locale, "salonpostam.update.members_updated", { count: i }))
}).catch(console.error)
}

View File

@@ -1,8 +1,7 @@
import { Events } from 'discord.js'
import { Events } from "discord.js"
import { logConsoleError } from "@/utils/console"
export default {
name: Events.Error,
execute(error: Error) {
console.error(error)
}
export const name = Events.Error
export function execute(error: Error) {
logConsoleError('discordjs', 'error', { message: error.message }, error)
}

View File

@@ -1,14 +1,12 @@
import { Events, Guild } from 'discord.js'
import dbGuildInit from '../../utils/dbGuildInit'
export default {
name: Events.GuildCreate,
async execute(guild: Guild) {
console.log(`Joined "${guild.name}" with ${guild.memberCount} members`)
let guildProfile = await dbGuildInit(guild)
if (!guildProfile) return console.log(`An error occured while initializing database data for "${guild.name}" !`)
console.log(`Database data for new guild "${guildProfile.guildName}" successfully initialized !`)
}
}
import { Events, Guild } from "discord.js"
import dbGuildInit from "@/utils/dbGuildInit"
import { logConsole } from "@/utils/console"
export const name = Events.GuildCreate
export async function execute(guild: Guild) {
logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() })
const guildProfile = await dbGuildInit(guild)
logConsole('mongoose', 'guild_create', { name: guildProfile.guildName })
}

View File

@@ -1,44 +1,42 @@
import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js'
export default {
name: Events.GuildMemberAdd,
async execute(member: GuildMember) {
if (member.guild.id === '1086577543651524699') { // Salon posé tamisé
let guild = member.guild
guild.members.fetch().then(() => {
let i = 0
guild.members.cache.forEach(async member => { if (!member.user.bot) i++ })
let channel = guild.channels.cache.get('1091140609139560508')
if (!channel) return
channel.setName('Changement...')
channel.setName(`${i} Gens Posés`)
}).catch(console.error)
} else if (member.guild.id === '796327643783626782') { // Jujul Community
let guild = member.guild
let channel = guild.channels.cache.get('837248593609097237') as TextChannel
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "837248593609097237" !`)
if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`)
let embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(`Salut ${member.user.username} !`)
.setDescription(`
Bienvenue sur le serveur de **Jujul** !
Nous sommes actuellement ${guild.memberCount} membres !\n
N'hésite pas à aller lire le <#797471924367786004> et à aller te présenter dans <#837138238417141791> !\n
Si tu as des questions,
n'hésite pas à les poser dans le <#837110617315344444> !\n
Bon séjour parmi nous !
`)
.setThumbnail(member.user.avatarURL())
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
}
}
}
import { Events, EmbedBuilder, ChannelType } from "discord.js"
import type { GuildMember } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
export const name = Events.GuildMemberAdd
export async function execute(member: GuildMember) {
if (member.guild.id === "1086577543651524699") {
// Salon posé tamisé
const guild = member.guild
guild.members.fetch().then(async () => {
let i = 0
guild.members.cache.forEach(member => { if (!member.user.bot) i++ })
const channel = guild.channels.cache.get("1091140609139560508")
if (!channel) return
await channel.setName("Changement...")
await channel.setName(`${i} Gens Posés`)
}).catch(console.error)
} else if (member.guild.id === "796327643783626782") {
// Jujul Community
const guild = member.guild
if (!guild.members.me) return
const channel = guild.channels.cache.get("837248593609097237")
if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'guild_member_add', { channelId: '837248593609097237' })
return
}
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(t(guild.preferredLocale, "welcome.title", { username: member.user.username }))
.setDescription(t(guild.preferredLocale, "welcome.description", { memberCount: guild.memberCount.toString() }))
.setThumbnail(member.user.avatarURL())
.setTimestamp(new Date())
return channel.send({ embeds: [embed] })
}
}

View File

@@ -1,21 +1,22 @@
import { Events, GuildMember } from 'discord.js'
export default {
name: Events.GuildMemberRemove,
async execute(member: GuildMember) {
if (member.guild.id === '1086577543651524699') { // Salon posé tamisé
let guild = member.guild
guild.members.fetch().then(() => {
let i = 0
guild.members.cache.forEach(async member => { if (!member.user.bot) i++ })
let channel = guild.channels.cache.get('1091140609139560508')
if (!channel) return
channel.setName('Changement...')
channel.setName(`${i} Gens Posés`)
}).catch(console.error)
}
}
}
import { Events } from "discord.js"
import type { GuildMember } from "discord.js"
import { t } from "@/utils/i18n"
export const name = Events.GuildMemberRemove
export function execute(member: GuildMember) {
if (member.guild.id === "1086577543651524699") {
// Salon posé tamisé
const guild = member.guild
guild.members.fetch().then(async () => {
let i = 0
guild.members.cache.forEach(member => { if (!member.user.bot) i++ })
const channel = guild.channels.cache.get("1091140609139560508")
if (!channel) return
await channel.setName(t(guild.preferredLocale, "salonpostam.update.loading"))
await channel.setName(t(guild.preferredLocale, "salonpostam.update.members_updated", { count: i.toString() }))
}).catch(console.error)
}
}

View File

@@ -1,35 +1,37 @@
import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js'
export default {
name: Events.GuildMemberUpdate,
async execute(oldMember: GuildMember, newMember: GuildMember) {
if (newMember.guild.id === '796327643783626782') { // Jujul Community
let guild = newMember.guild
let channel = guild.channels.cache.get('924353449930412153') as TextChannel
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`)
let boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`)
const hadRole = oldMember.roles.cache.find(role => role.id === boostRole.id)
const hasRole = newMember.roles.cache.find(role => role.id === boostRole.id)
if (!hadRole && hasRole) {
if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`)
let embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(`Nouveau boost de ${newMember.user.username} !`)
.setDescription(`
Merci à toi pour ce boost.\n
Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts !
`)
.setThumbnail(newMember.user.avatarURL())
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
}
}
}
}
import { Events, EmbedBuilder, ChannelType } from "discord.js"
import type { GuildMember } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
export const name = Events.GuildMemberUpdate
export async function execute(oldMember: GuildMember, newMember: GuildMember) {
if (newMember.guild.id === "796327643783626782") {
// Jujul Community
const guild = newMember.guild
const channel = await guild.channels.fetch("924353449930412153")
if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" })
return
}
const boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return }
const hadRole = oldMember.roles.cache.find(role => role.id === boostRole.id)
const hasRole = newMember.roles.cache.find(role => role.id === boostRole.id)
if (!hadRole && hasRole) {
if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return }
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(t(guild.preferredLocale, "boost.new_boost_title", { username: newMember.user.username }))
.setDescription(t(guild.preferredLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" }))
.setThumbnail(newMember.user.avatarURL())
.setTimestamp(new Date())
return channel.send({ embeds: [embed] })
}
}
}

View File

@@ -1,18 +1,18 @@
import { Events, Guild } from 'discord.js'
import dbGuildInit from '../../utils/dbGuildInit'
import dbGuild from '../../schemas/guild'
export default {
name: Events.GuildUpdate,
async execute(oldGuild: Guild, newGuild: Guild) {
console.log(`Guild ${oldGuild.name} updated`)
let guildProfile = await dbGuild.findOne({ guildId: newGuild.id })
if (!guildProfile) guildProfile = await dbGuildInit(newGuild)
else {
guildProfile.guildName = newGuild.name
guildProfile.guildIcon = newGuild.iconURL() ?? 'None'
await guildProfile.save().catch(console.error)
}
}
}
import { Events } from "discord.js"
import type { Guild } from "discord.js"
import dbGuildInit from "@/utils/dbGuildInit"
import dbGuild from "@/schemas/guild"
import { logConsole } from "@/utils/console"
export const name = Events.GuildUpdate
export async function execute(oldGuild: Guild, newGuild: Guild) {
logConsole('discordjs', 'guild_update', { name: oldGuild.name })
let guildProfile = await dbGuild.findOne({ guildId: newGuild.id })
if (!guildProfile) guildProfile = await dbGuildInit(newGuild)
else {
guildProfile.guildName = newGuild.name
guildProfile.guildIcon = newGuild.iconURL() ?? "None"
await guildProfile.save().catch(console.error)
}
}

View File

@@ -0,0 +1,21 @@
import * as error from "./error"
import * as guildCreate from "./guildCreate"
import * as guildMemberAdd from "./guildMemberAdd"
import * as guildMemberRemove from "./guildMemberRemove"
import * as guildMemberUpdate from "./guildMemberUpdate"
import * as guildUpdate from "./guildUpdate"
import * as interactionCreate from "./interactionCreate"
import * as ready from "./ready"
import type { Event } from "@/types"
export default [
error,
guildCreate,
guildMemberAdd,
guildMemberRemove,
guildMemberUpdate,
guildUpdate,
interactionCreate,
ready
] as Event[]

View File

@@ -1,45 +1,49 @@
import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction } from 'discord.js'
import { playerButtons, playerEdit } from '../../utils/player'
export default {
name: Events.InteractionCreate,
async execute(interaction: Interaction) {
//if (!interaction.isAutocomplete() && !interaction.isChatInputCommand() && !interaction.isButton()) return console.error(`Interaction ${interaction.commandName} is not a command.`)
if (interaction.isChatInputCommand()) {
interaction = interaction as ChatInputCommandInteraction
let chatInputCommand = interaction.client.commands.get(interaction.commandName)
if (!chatInputCommand) return console.error(`No chat input command matching ${interaction.commandName} was found.`)
console.log(`Command '${interaction.commandName}' launched by ${interaction.user.tag}`)
try { await chatInputCommand.execute(interaction) }
catch (error) { console.error(`Error executing ${interaction.commandName}:`, error) }
}
else if (interaction.isAutocomplete()) {
interaction = interaction as AutocompleteInteraction
let autoCompleteRun = interaction.client.commands.get(interaction.commandName)
if (!autoCompleteRun) return console.error(`No autoCompleteRun matching ${interaction.commandName} was found.`)
console.log(`AutoCompleteRun '${interaction.commandName}' launched by ${interaction.user.tag}`)
try { await autoCompleteRun.autocompleteRun(interaction) }
catch (error) { console.error(`Error autocompleting ${interaction.commandName}:`, error) }
}
else if (interaction.isButton()) {
interaction = interaction as ButtonInteraction
let button = interaction.client.buttons.get(interaction.customId)
if (!button) return console.error(`No button id matching ${interaction.customId} was found.`)
console.log(`Button '${interaction.customId}' clicked by ${interaction.user.tag}`)
if (playerButtons.includes(interaction.customId)) { await playerEdit(interaction) }
try { await button.execute(interaction) }
catch (error) { console.error(`Error clicking ${interaction.customId}:`, error) }
}
}
}
import { Events } from "discord.js"
import type { Interaction } from "discord.js"
import commands from "@/commands"
import buttons, { buttonFolders } from "@/buttons"
import selectMenus from "@/selectmenus"
import { playerEdit } from "@/utils/player"
import { logConsole, logConsoleError } from "@/utils/console"
export const name = Events.InteractionCreate
export async function execute(interaction: Interaction) {
if (interaction.isChatInputCommand()) {
const chatInputCommand = commands.find(cmd => cmd.data.name == interaction.commandName)
if (!chatInputCommand) { logConsole('discordjs', 'interaction_create.command_not_found', { command: interaction.commandName }); return }
logConsole('discordjs', 'interaction_create.command_launched', { command: interaction.commandName, user: interaction.user.tag })
try { await chatInputCommand.execute(interaction) }
catch (error) { logConsoleError('discordjs', 'interaction_create.command_error', { command: interaction.commandName }, error as Error) }
}
else if (interaction.isAutocomplete()) {
const autocompleteRun = commands.find(cmd => cmd.data.name == interaction.commandName)
if (!autocompleteRun?.autocompleteRun) { logConsole('discordjs', 'interaction_create.autocomplete_not_found', { command: interaction.commandName }); return }
logConsole('discordjs', 'interaction_create.autocomplete_launched', { command: interaction.commandName, user: interaction.user.tag })
try { await autocompleteRun.autocompleteRun(interaction) }
catch (error) { logConsoleError('discordjs', 'interaction_create.autocomplete_error', { command: interaction.commandName }, error as Error) }
}
else if (interaction.isButton()) {
const button = buttons.find(btn => btn.id === interaction.customId)
if (!button) { logConsole('discordjs', 'interaction_create.button_not_found', { id: interaction.customId }); return }
logConsole('discordjs', 'interaction_create.button_clicked', { id: interaction.customId, user: interaction.user.tag })
try { await button.execute(interaction) }
catch (error) { logConsoleError('discordjs', 'interaction_create.button_error', { id: interaction.customId }, error as Error) }
if (buttonFolders.find(folder => folder.name === "player" ? folder.commands.some(cmd => cmd.id === interaction.customId) : false)) await playerEdit(interaction)
}
else if (interaction.isAnySelectMenu()) {
const selectMenu = selectMenus.find(menu => menu.id === interaction.customId)
if (!selectMenu) { logConsole('discordjs', 'interaction_create.selectmenu_not_found', { id: interaction.customId }); return }
logConsole('discordjs', 'interaction_create.selectmenu_used', { id: interaction.customId, user: interaction.user.tag })
try { await selectMenu.execute(interaction) }
catch (error) { logConsoleError('discordjs', 'interaction_create.selectmenu_error', { id: interaction.customId }, error as Error) }
}
}

View File

@@ -1,117 +1,139 @@
import { Events, Client, ActivityType } from 'discord.js'
import { SpotifyExtractor } from '@discord-player/extractor'
import { YoutubeiExtractor } from 'discord-player-youtubei'
import { useMainPlayer } from 'discord-player'
import { connect } from 'mongoose'
import WebSocket from 'websocket'
import chalk from 'chalk'
import 'dotenv/config'
import dbGuildInit from '../../utils/dbGuildInit'
import dbGuild from '../../schemas/guild'
import { playerDisco, playerReplay } from '../../utils/player'
import * as Twitch from '../../utils/twitch'
import rss from '../../utils/rss'
export default {
name: Events.ClientReady,
once: true,
async execute(client: Client) {
console.log(chalk.blue(`[DiscordJS] Connected to Discord ! Logged in as ${client.user?.tag ?? 'unknown'}`))
client.user?.setActivity('some bangers...', { type: ActivityType.Listening })
await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Spotify extractor loaded.'))).catch(console.error)
await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Youtube extractor loaded.'))).catch(console.error)
let mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}`
await connect(mongo_url).catch(console.error)
let guilds = client.guilds.cache
guilds.forEach(async guild => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (!guildProfile) guildProfile = await dbGuildInit(guild)
if (guildProfile.guildPlayer?.replay?.enabled && guildProfile.guildPlayer?.replay?.textChannelId) await playerReplay(client, guildProfile)
client.disco = { interval: {} as NodeJS.Timeout }
client.disco.interval = setInterval(async () => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (guildProfile?.guildPlayer?.disco?.enabled) {
let state = await playerDisco(client, guildProfile)
if (state === 'clear') clearInterval(client.disco.interval)
}
}, 3000)
client.rss = { interval: {} as NodeJS.Timeout }
client.rss.interval = setInterval(async () => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (guildProfile?.guildRss?.enabled) {
let state = await rss(client, guildProfile)
if (state === 'clear') clearInterval(client.rss.interval)
}
}, 30000)
// TWITCH EVENTSUB
if (process.env['TWITCH_RUNNING_' + guild.id]) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Already running...`))
console.log(chalk.magenta(`[Twitch] {${guild.name}} Not running, starting...`))
process.env['TWITCH_RUNNING_' + guild.id] = 'true'
let client_id = process.env.TWITCH_APP_ID as string
let client_secret = process.env.TWITCH_APP_SECRET as string
if (!client_id || !client_secret) return console.log(chalk.magenta(`[Twitch] {${guild.name}} App ID or Secret is not defined !`))
let dbData = guildProfile.get('guildTwitch')
if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled, please activate with \`/database edit guildTwitch.enabled True\` !`))
let twitch = new WebSocket.client().on('connect', async connection => {
console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub WebSocket Connected !`))
connection.on('message', async message => { if (message.type === 'utf8') { try {
let data = JSON.parse(message.utf8Data)
let channel_access_token = guildProfile.get('guildTwitch')?.channelAccessToken as string
// Check when Twitch asks to login
if (data.metadata.message_type === 'session_welcome') {
// Check if the channel access token is still valid before connecting
channel_access_token = await Twitch.checkChannel(client_id, client_secret, channel_access_token, guild) as string
if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`))
// Get broadcaster user id and reward id
let broadcaster_user_id = await Twitch.getUserInfo(client_id, channel_access_token, 'id') as string
let topics: { [key: string]: { version: string; condition: { broadcaster_user_id: string } } } = {
'stream.online': { version: '1', condition: { broadcaster_user_id } },
'stream.offline': { version: '1', condition: { broadcaster_user_id } }
}
// Subscribe to all events required
for (let type in topics) {
console.log(chalk.magenta(`[Twitch] {${guild.name}} Creating ${type}...`))
let { version, condition } = topics[type]
let status = await Twitch.subscribeToEvents(client_id, channel_access_token, data.payload.session.id, type, version, condition)
if (!status) return console.error(chalk.magenta(`[Twitch] {${guild.name}} Failed to create ${type}`))
else if (status.error) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Erreur de connexion EventSub, veuillez vous reconnecter !`))
else console.log(chalk.magenta(`[Twitch] {${guild.name}} Successfully created ${type}`))
}
}
// Handle notification messages
else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild)
} catch (error) { console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)) } } })
.on('error', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)))
.on('close', () => {
console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub Connection Closed !`))
twitch.connect('wss://eventsub.wss.twitch.tv/ws')
})
}).on('connectFailed', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)))
twitch.connect('wss://eventsub.wss.twitch.tv/ws')
})
}
}
import { Events, ActivityType, ChannelType } from "discord.js"
import type { Client } from "discord.js"
import { useMainPlayer } from "discord-player"
import { SpotifyExtractor } from "@discord-player/extractor"
import { YoutubeiExtractor } from "discord-player-youtubei"
import { connect } from "mongoose"
import type { Document } from "mongoose"
import { playerDisco, playerReplay } from "@/utils/player"
import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch"
import { logConsole } from "@/utils/console"
import type { GuildPlayer, Disco, GuildTwitch, GuildFbx } from "@/types/schemas"
import * as Freebox from "@/utils/freebox"
import dbGuildInit from "@/utils/dbGuildInit"
import dbGuild from "@/schemas/guild"
export const name = Events.ClientReady
export const once = true
export async function execute(client: Client) {
logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" })
client.user?.setActivity("some bangers...", { type: ActivityType.Listening })
await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error)
await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error)
const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}`
await connect(mongo_url).catch(console.error)
if (process.env.NODE_ENV === "development") await twitchClient.eventSub.deleteAllSubscriptions()
const streamerIds: string[] = []
await Promise.all(client.guilds.cache.map(async guild => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
guildProfile ??= await dbGuildInit(guild)
const dbDataPlayer = guildProfile.get("guildPlayer") as GuildPlayer
const botInstance = dbDataPlayer.instances?.find(instance => instance.botId === client.user?.id)
if (botInstance?.replay.trackUrl) await playerReplay(client, dbDataPlayer)
client.disco = { interval: {} as NodeJS.Timeout }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
client.disco.interval = setInterval(async () => {
const guildProfile = await dbGuild.findOne({ guildId: guild.id })
const dbDataDisco = guildProfile?.get("guildPlayer.disco") as Disco
if (dbDataDisco.enabled) {
const state = await playerDisco(client, guild, dbDataDisco)
if (state === "clear") clearInterval(client.disco.interval)
}
}, 3000)
// Gestion du timer LCD Freebox
const dbDataFbx = guildProfile.get("guildFbx") as GuildFbx
if (dbDataFbx.enabled && dbDataFbx.lcd) {
if (dbDataFbx.lcd.enabled && dbDataFbx.lcd.botId === client.user?.id) {
logConsole('freebox', 'lcd_timer_restored', { guild: guild.name })
Freebox.Timer.schedule(client, guild.id, dbDataFbx)
}
}
const dbDataTwitch = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbDataTwitch.enabled) return
if (!dbDataTwitch.streamers.length) { logConsole('twitch', 'ready.no_streamers_configured', { guild: guild.name }); return }
await Promise.all(dbDataTwitch.streamers.map(async streamer => {
if (streamerIds.includes(streamer.twitchUserId)) return
streamerIds.push(streamer.twitchUserId)
const user = await twitchClient.users.getUserById(streamer.twitchUserId)
if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return }
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId)
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
listener.onStreamOnline(streamer.twitchUserId, onlineSub)
logConsole('twitch', 'listener_registered', { type: 'stream.online', name: user.name, id: streamer.twitchUserId })
}
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
listener.onStreamOffline(streamer.twitchUserId, offlineSub)
logConsole('twitch', 'listener_registered', { type: 'stream.offline', name: user.name, id: streamer.twitchUserId })
}
logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId })
const stream = await user.getStream()
if (stream && streamer.messageId) {
logConsole('twitch', 'ready.stream_restoration', { guild: guild.name, userName: user.name, userId: streamer.twitchUserId })
// Vérifier que le message existe encore
if (!dbDataTwitch.channelId) return
const channel = await guild.channels.fetch(dbDataTwitch.channelId)
if (channel && (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement)) {
try {
await channel.messages.fetch(streamer.messageId)
startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId)
logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name })
} catch (error) {
logConsole('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name })
console.error(error)
await cleanupMessageId(guildProfile, streamer.twitchUserId)
}
}
} else if (streamer.messageId) {
// Il y a un messageId mais le stream n'est plus en ligne, nettoyer
logConsole('twitch', 'ready.stream_offline_cleanup', { guild: guild.name, userName: user.name })
await cleanupMessageId(guildProfile, streamer.twitchUserId)
}
logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId })
}))
}))
const subs = await twitchClient.eventSub.getSubscriptions()
await Promise.all(subs.data.map(async sub => {
if (streamerIds.includes(sub.condition.broadcaster_user_id as string)) return
if (sub.type !== "stream.online" && sub.type !== "stream.offline") return
await sub.unsubscribe().catch(console.error)
logConsole('twitch', 'unsubscribed', { type: sub.type, id: sub.condition.broadcaster_user_id as string })
}))
}
async function cleanupMessageId(guildProfile: Document, twitchUserId: string) {
try {
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === twitchUserId)
if (streamerIndex === -1) return
dbData.streamers[streamerIndex].messageId = ""
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save()
} catch (error) {
logConsole('twitch', 'ready.cleanup_error', { userId: twitchUserId })
console.error(error)
}
}

View File

@@ -1,61 +0,0 @@
//import { Events, VoiceState } from 'discord.js'
import { Events } from 'discord.js'
export default {
name: Events.VoiceStateUpdate,
async execute() {
//async execute(oldState: VoiceState, newState: VoiceState) {
/*
let oldMute = oldState.serverMute
let newMute = newState.serverMute
let oldDeaf = oldState.serverDeaf
let newDeaf = newState.serverDeaf
let oldChannel = oldState.channelId
let newChannel = newState.channelId
console.log(oldChannel)
console.log(newChannel)
let guild = newState.guild
let member = newState.member
let channel = guild.channels.cache.get('1076215868863819848')
let angels = guild.members.cache.get('223831938346123275')
if (oldChannel !== newChannel) {
let executor = await logMoveOrKick('channel_id')
//if (!executor) channel.send(`Impossible de savoir qui a déplacé <@${member.id}> !`)
//else if (member.id === executor.id) channel.send(`<@${member.id}> s'est déplacé lui-même le con...`)
//else {
// channel.send(`<@${member.id}> a été mis en sourdine par <@${executor.id}> !`)
//}
} else if (!oldMute && newMute) {
let executor = await logMuteOrDeaf('mute')
if (!executor) channel.send(`Impossible de savoir qui a muté <@${member.id}> !`)
else if (member.id === executor.id) channel.send(`<@${member.id}> s'est muté lui-même le con...`)
else {
channel.send(`<@${member.id}> a été muté par <@${executor.id}> !`)
}
} else if (!oldDeaf && newDeaf) {
let executor = await logMuteOrDeaf('deaf')
if (!executor) channel.send(`Impossible de savoir qui a mis en sourdine <@${member.id}> !`)
else if (member.id === executor.id) channel.send(`<@${member.id}> s'est mis en sourdine lui-même le con...`)
else {
channel.send(`<@${member.id}> a été mis en sourdine par <@${executor.id}> !`)
}
}
async function logMoveOrKick() {
let auditLogs = await guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberMove })
console.log(auditLogs.entries.find(entry => { return entry }))
let log = await auditLogs.entries.find(entry => { return entry.extra.channel.id === newChannel })
console.log(log)
if (!log) return undefined
let executor = await guild.members.cache.get(log.executor.id)
return executor
}
async function logMuteOrDeaf(type) {
let auditLogs = await guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberUpdate })
let log = await auditLogs.entries.find(entry => { return entry.target.id === member.id && entry.changes[0].key === type && entry.changes[0].new === true })
if (!log) return undefined
let executor = await guild.members.cache.get(log.executor.id)
return executor
}
*/
}
}

View File

@@ -1,8 +1,6 @@
import chalk from 'chalk'
import { logConsole } from "@/utils/console"
export default {
name: 'connected',
async execute() {
console.log(chalk.green('[Mongoose] Connected to MongoDB !'))
}
}
export const name = "connected"
export function execute() {
logConsole('mongoose', 'connected')
}

View File

@@ -1,8 +1,6 @@
import chalk from 'chalk'
import { logConsole } from "@/utils/console"
export default {
name: 'connecting',
async execute() {
console.log(chalk.green('[Mongoose] Connecting to MongoDB...'))
}
}
export const name = "connecting"
export function execute() {
logConsole('mongoose', 'connecting')
}

View File

@@ -1,8 +1,6 @@
import chalk from 'chalk'
import { logConsole } from "@/utils/console"
export default {
name: 'disconnected',
async execute() {
console.log(chalk.green('[Mongoose] Disconnected from MongoDB !'))
}
}
export const name = "disconnected"
export function execute() {
logConsole('mongoose', 'disconnected')
}

View File

@@ -1,8 +1,6 @@
import chalk from 'chalk'
import { logConsoleError } from "@/utils/console"
export default {
name: 'error',
async execute(error: Error) {
console.log(chalk.red('[Mongoose] An error occured with the database conenction :\n' + error))
}
}
export const name = "error"
export function execute(error: Error) {
logConsoleError('mongoose', 'error', { message: error.message }, error)
}

13
src/events/mongo/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import * as connected from "./connected"
import * as connecting from "./connecting"
import * as disconnected from "./disconnected"
import * as error from "./error"
import type { Event } from "@/types"
export default [
connected,
connecting,
disconnected,
error
] as Event[]

View File

@@ -1,10 +1,11 @@
import { GuildQueue, Track } from 'discord-player'
import { PlayerMetadata } from '../../utils/player'
export default {
name: 'audioTrackAdd',
async execute(queue: GuildQueue<PlayerMetadata>, track: Track) {
// Emitted when the player adds a single song to its queue
queue.metadata.channel.send(`Musique **${track.title}** de **${track.author}** ajoutée à la file d'attente !`)
}
}
import type { GuildQueue, Track } from "discord-player"
import type { PlayerMetadata } from "@/types/player"
import { t } from "@/utils/i18n"
export const name = "audioTrackAdd"
export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) {
// Emitted when the player adds a single song to its queue
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added", { title: track.title }) })
}

View File

@@ -1,10 +1,10 @@
import { GuildQueue, Track } from 'discord-player'
import { PlayerMetadata } from '../../utils/player'
export default {
name: 'audioTracksAdd',
async execute(queue: GuildQueue<PlayerMetadata>, track: Array<Track>) {
// Emitted when the player adds multiple songs to its queue
queue.metadata.channel.send(`Ajout de ${track.length} musiques à la file d'attente !`)
}
}
import type { GuildQueue, Track } from "discord-player"
import type { PlayerMetadata } from "@/types/player"
import { t } from "@/utils/i18n"
export const name = "audioTracksAdd"
export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track[]) {
// Emitted when the player adds multiple songs to its queue
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added_playlist", { count: track.length.toString() }) })
}

View File

@@ -1,10 +1,9 @@
import { GuildQueue } from 'discord-player'
export default {
name: 'debug',
async execute(queue: GuildQueue, message: string) {
// Emitted when the player queue sends debug info
// Useful for seeing what state the current queue is at
console.log(`Player debug event: ${message}`)
}
}
import type { GuildQueue } from "discord-player"
import { logConsoleDev } from "@/utils/console"
export const name = "debug"
export function execute(queue: GuildQueue, message: string) {
// Emitted when the player queue sends debug info
// Useful for seeing what state the current queue is at
logConsoleDev('discord_player', 'debug', { message })
}

View File

@@ -1,24 +1,13 @@
import { GuildQueue } from 'discord-player'
import { PlayerMetadata } from '../../utils/player'
import dbGuild from '../../schemas/guild'
export default {
name: 'disconnect',
async execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the bot leaves the voice channel
queue.metadata.channel.send("J'ai quitté le vocal !")
let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id })
if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`)
let dbData = guildProfile.get('guildPlayer.replay')
dbData['textChannelId'] = ''
dbData['voiceChannelId'] = ''
dbData['trackUrl'] = ''
dbData['progress'] = ''
guildProfile.set('guildPlayer.replay', dbData)
guildProfile.markModified('guildPlayer.replay')
return await guildProfile.save().catch(console.error)
}
}
import type { GuildQueue } from "discord-player"
import type { PlayerMetadata } from "@/types/player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export const name = "disconnect"
export async function execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the bot leaves the voice channel
await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "")
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.disconnect") })
}

View File

@@ -1,11 +1,14 @@
import { GuildQueue } from 'discord-player'
import { PlayerMetadata } from '../../utils/player'
export default {
name: 'emptyChannel',
async execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the voice channel has been empty for the set threshold
// Bot will automatically leave the voice channel with this event
queue.metadata.channel.send(`Je quitte le vocal car il est vide depuis trop longtemps.`)
}
}
import type { GuildQueue } from "discord-player"
import type { PlayerMetadata } from "@/types/player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export const name = "emptyChannel"
export async function execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the voice channel has been empty for the set threshold
// Bot will automatically leave the voice channel with this event
await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "")
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.leaving_empty_channel") })
}

View File

@@ -1,10 +1,13 @@
import { GuildQueue } from 'discord-player'
import { PlayerMetadata } from '../../utils/player'
export default {
name: 'emptyQueue',
async execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the player queue has finished
queue.metadata.channel.send("File d'attente vide !")
}
}
import type { GuildQueue } from "discord-player"
import type { PlayerMetadata } from "@/types/player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export const name = "emptyQueue"
export async function execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the player queue has finished
await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "")
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.queue_empty") })
}

Some files were not shown because too many files have changed in this diff Show More