Compare commits

...

6 Commits

Author SHA1 Message Date
9a4902291e Fix workflow sha tag
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m2s
2025-06-10 01:49:29 +02:00
d06df32bab Fix workflow tags and remove attestation
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 3m11s
2025-06-09 23:42:02 +02:00
60d0c01212 Fix chmod
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 5m5s
2025-06-09 19:10:35 +02:00
5e7c1842a4 Fix path + ffmpeg
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 5m40s
2025-06-09 16:55:41 +02:00
ddd617317c Réécriture complète 4.0
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m16s
2025-06-09 16:29:12 +02:00
f2c6388da6 Fix env variable
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 50s
2025-05-30 15:35:57 +02:00
135 changed files with 8102 additions and 4344 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

@@ -12,7 +12,7 @@ on:
env: env:
REGISTRY: rgy.angels-dev.fr REGISTRY: rgy.angels-dev.fr
PATH: prod IMAGE_PATH: prod
IMAGE_NAME: bot_tamiseur IMAGE_NAME: bot_tamiseur
jobs: jobs:
@@ -26,6 +26,16 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -36,41 +46,34 @@ jobs:
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata - name: Extract metadata for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }}
tags: | tags: |
# Tag avec le nom de la branche
type=ref,event=branch
# Tag avec le nom du tag Git # Tag avec le nom du tag Git
type=ref,event=tag type=ref,event=tag
# Tag avec le SHA du commit # Tag 'latest' pour la branche master
type=sha,prefix={{branch}}-
# Tag latest pour la branche master
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
# Tag avec le SHA pour les autres branches
type=sha,prefix=sha-
labels: | labels: |
org.opencontainers.image.title=${{ env.IMAGE_NAME }} org.opencontainers.image.title=${{ env.IMAGE_NAME }}
org.opencontainers.image.description=Bot Discord org.opencontainers.image.description=Bot Discord de moi
org.opencontainers.image.url=https://gitea.zac.ovh/zachary/bot_Tamiseur org.opencontainers.image.url=https://git.zac.ovh/zachary/bot_Tamiseur
org.opencontainers.image.source=https://gitea.zac.ovh/zachary/bot_Tamiseur org.opencontainers.image.source=https://git.zac.ovh/zachary/bot_Tamiseur
org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created={{date 'RFC3339'}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 # Multi-architecture si nécessaire file: build/node.dockerfile
platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

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/ dist/
node_modules/ node_modules/
public/cracks/* public/cracks/
.env*
.ncurc.json

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 # Build new application
# ===================== # =====================
.PHONY: tag-build .PHONY: tag-build
tag-build: ## DEV : Build a prod version with a timestamped tag tag-build: ## DEV : Build a prod version with a timestamped tag
@export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \ @export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \

128
README.md Executable file → Normal file
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

23
build/node.dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# Starting from node
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
RUN apk add --no-cache ffmpeg 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 strategy: RollingUpdate
image: image:
repository: "rgy.angels-dev.fr/prod/bot_tamiseur" repository: "rgy.angels-dev.fr/prod/bot_tamiseur"
tag: "3.0.4" tag: "4.0.0"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
env: env:
NODE_ENV: "production" 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 eslint from "@eslint/js"
import tsParser from "@typescript-eslint/parser" import tseslint from "typescript-eslint"
import { FlatCompat } from "@eslint/eslintrc"
import { fileURLToPath } from "node:url"
import path from "node:path"
import js from "@eslint/js"
const __filename = fileURLToPath(import.meta.url) export default tseslint.config(
const __dirname = path.dirname(__filename) eslint.configs.recommended,
const compat = new FlatCompat({ tseslint.configs.recommendedTypeChecked,
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
})
export default [
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
{ {
plugins: { "@typescript-eslint": typescriptEslint }, languageOptions: {
languageOptions: { parser: tsParser }, parserOptions: {
rules: { "prefer-const": "off" } 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", "name": "bot_tamiseur",
"description": "Listen to music and use fun commands with your friends!", "description": "Listen to music and use fun commands with your friends!",
"version": "3.0.4", "version": "4.0.0",
"author": { "author": {
"name": "Zachary Guénot" "name": "Zachary Guénot"
}, },
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"format": "prettier --write .", "start": "node index.js",
"start": "npx tsx src/index.ts", "start:prod": "NODE_ENV=production node dist/index.js",
"dev": "nodemon -e ts src/index.ts", "start:dev": "NODE_ENV=development tsx src/index.ts",
"build": "tsc", "dev": "NODE_ENV=development tsx watch src/index.ts",
"lint": "eslint src/**/*.ts", "lint": "eslint .",
"prod": "node dist/index.js" "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": { "dependencies": {
"@discord-player/equalizer": "^7.0.0", "@discord-player/extractor": "^7.1.0",
"@discord-player/extractor": "^7.0.0",
"@discordjs/voice": "^0.18.0", "@discordjs/voice": "^0.18.0",
"@evan/opus": "^1.0.3", "@twurple/api": "^7.3.0",
"axios": "^1.7.9", "@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", "bufferutil": "^4.0.9",
"chalk": "^4.1.2", "chalk": "^5.4.1",
"discord-player": "^7.0.0", "discord-player": "^7.1.0",
"discord-player-youtubei": "^1.3.7", "discord-player-youtubei": "^1.4.6",
"discord.js": "^14.17.2", "discord.js": "^14.19.3",
"dotenv": "^16.4.7",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jsdom": "^25.0.1",
"libsodium-wrappers": "^0.7.15",
"mediaplex": "^1.0.0", "mediaplex": "^1.0.0",
"mongoose": "^8.9.3", "mongoose": "^8.15.1",
"parse-torrent": "^9.1.5", "parse-torrent": "^9.1.5",
"require-all": "^3.0.0", "zlib-sync": "^0.1.10"
"rss-parser": "^3.13.0",
"utf-8-validate": "^6.0.5",
"websocket": "^1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.28.0",
"@eslint/js": "^9.17.0", "@types/node": "^22.15.30",
"@swc/core": "^1.10.4",
"@types/node": "^22.10.5",
"@types/parse-torrent": "^5.8.7", "@types/parse-torrent": "^5.8.7",
"@types/websocket": "^1.0.10", "dotenv": "^16.5.0",
"@typescript-eslint/eslint-plugin": "^8.19.0", "eslint": "^9.28.0",
"@typescript-eslint/parser": "^8.19.0", "tsup": "^8.5.0",
"eslint": "^9.17.0", "tsx": "^4.19.4",
"nodemon": "^3.1.9", "typescript": "^5.8.3",
"prettier": "^3.4.2", "typescript-eslint": "^8.33.1"
"tsx": "^4.19.2"
} }
} }

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 })
}
}

383
src/commands/global/amp.ts Executable file → Normal file
View File

@@ -1,210 +1,241 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, EmbedBuilder, inlineCode, PermissionFlagsBits } from 'discord.js' import { SlashCommandBuilder, EmbedBuilder, inlineCode, PermissionFlagsBits, MessageFlags } from "discord.js"
import dbGuild from '../../schemas/guild' import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, Locale } from "discord.js"
import * as AMP from '../../utils/amp' 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"
interface InstanceFields { function returnMsg(result: ReturnMsgData, locale: Locale) {
name: string if (result.status === "fail") return `${t(locale, "common.failed")}\n${inlineCode(`${result.Title}: ${result.Message}`)}`
value: string if (result.status === "error") return `${t(locale, "common.error_occurred")}\n${inlineCode(`${result.error_code}`)}`
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}`)}` } export const data = new SlashCommandBuilder()
function errorMsg(data: ErrorMsgData) { return `Y'a eu une erreur !\n${inlineCode(`${data.error_code}`)}` } .setName("amp")
.setDescription("Access my AMP gaming panel")
export default { .setDescriptionLocalizations({ fr: "Accède à mon panel de jeu AMP" })
data: new SlashCommandBuilder()
.setName('amp')
.setDescription('Accède à mon panel de jeu AMP !')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand.setName('login').setDescription("Connectez-vous avant d'effectuer une autre commande !") .addSubcommand(subcommand => subcommand
.addStringOption(option => option.setName('username').setDescription("Nom d'Utilisateur").setRequired(true)) .setName("login")
.addStringOption(option => option.setName('password').setDescription('Mot de Passe').setRequired(true)) .setDescription("Log in before performing another command")
.addBooleanOption(option => option.setName('remember').setDescription('Mémoriser les identifiants').setRequired(true)) .setNameLocalizations({ fr: "connexion" })
.addStringOption(option => option.setName('otp').setDescription('Code de double authentification'))) .setDescriptionLocalizations({ fr: "Connectez-vous avant d'effectuer une autre commande" })
.addSubcommandGroup(subcommandgroup => subcommandgroup.setName('instances').setDescription('Intéragir avec les instances AMP.') .addStringOption(option => option
.addSubcommand(subcommand => subcommand.setName('list').setDescription('Liste toutes les instances disponibles.')) .setName("username")
.addSubcommand(subcommand => subcommand.setName('manage').setDescription('Gérer une instance.') .setDescription("Username")
.addStringOption(option => option.setName('instance').setDescription("Nom de l'instance").setRequired(true).setAutocomplete(true))) .setNameLocalizations({ fr: "nom_utilisateur" })
.addSubcommand(subcommand => subcommand.setName('restart').setDescription('Redémarre une instance.') .setDescriptionLocalizations({ fr: "Nom d'utilisateur" })
.addStringOption(option => option.setName('name').setDescription("Nom de l'instance").setRequired(true))) .setRequired(true)
), )
async autocompleteRun(interaction: AutocompleteInteraction) { .addStringOption(option => option
let query = interaction.options.getString('instance', true) .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)
)
)
)
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) export async function execute(interaction: ChatInputCommandInteraction) {
if (!guildProfile) return await interaction.respond([]) 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 })
let dbData = guildProfile.get('guildAmp') const dbData = guildProfile.get("guildAmp") as GuildAmp
if (!dbData?.enabled) return await interaction.respond([]) if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral })
let host = dbData.host as string const host = dbData.host
let username = dbData.username as string if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral })
let sessionID = dbData.sessionID as string
let rememberMeToken = dbData.rememberMeToken as string
// Check if the SessionID is still valid let username = dbData.username
let session = await AMP.CheckSession(host, sessionID) let sessionID = dbData.sessionID
if (session.status === 'fail') { let rememberMeToken = dbData.rememberMeToken
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 const subcommandGroup = interaction.options.getSubcommandGroup(false)
else if (result.status === 'fail') return interaction.respond([]) const subcommand = interaction.options.getSubcommand(true)
else if (result.status === 'error') return interaction.respond([]) if (subcommand == "login") {
} 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 // Get a SessionID and a RememberMeToken if wanted
await interaction.deferReply({ ephemeral: true }) await interaction.deferReply({ flags: MessageFlags.Ephemeral })
let details = { const details = {
username: interaction.options.getString('username') || '', username: interaction.options.getString("username", true),
password: interaction.options.getString('password') || '', password: interaction.options.getString("password", true),
rememberMe: interaction.options.getBoolean('remember') || '', token: interaction.options.getString("otp") ?? "",
token: interaction.options.getString('otp') || '' rememberMe: interaction.options.getBoolean("remember", true)
} }
let result = await AMP.Core.Login(host, details) const result = await AMP.Core.Login(host, details)
if (result.status === 'success') { if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral })
username = dbData['username'] = result.data.userInfo.Username
sessionID = dbData['sessionID'] = result.data.sessionID
rememberMeToken = dbData['rememberMeToken'] = result.data.rememberMeToken
guildProfile.set('guildAmp', dbData) const loginData = result.data as LoginSuccessData
guildProfile.markModified('guildAmp') 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) await guildProfile.save().catch(console.error)
return await interaction.followUp(`Tu es connecté au panel sous **${username}** !`) return interaction.followUp({ content: t(interaction.locale, "amp.logged_in", { username }), flags: MessageFlags.Ephemeral })
}
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() await interaction.deferReply()
// Check if the SessionID is still valid // Check if the SessionID is still valid
let session = await AMP.CheckSession(host, sessionID) if (!sessionID) return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
if (session.status === 'fail') {
if (rememberMeToken) { const checkResult = await AMP.CheckSession(host, sessionID)
if (checkResult.status === "fail") {
if (rememberMeToken && username) {
// Refresh the SessionID if the RememberMeToken is available // Refresh the SessionID if the RememberMeToken is available
let details = { username, password: '', token: rememberMeToken, rememberMe: true } const details = { username, password: "", token: rememberMeToken, rememberMe: true }
let result = await AMP.Core.Login(host, details) const loginResult = await AMP.Core.Login(host, details)
if (loginResult.status !== "success") return interaction.followUp({ content: returnMsg(loginResult, interaction.locale), flags: MessageFlags.Ephemeral })
if (result.status === 'success') sessionID = result.data.sessionID const loginData = loginResult.data as LoginSuccessData
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) sessionID = loginData.sessionID
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 return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
} }
else if (session.status === 'error') return await interaction.followUp(errorMsg(session.data)) else if (checkResult.status === "error") return interaction.followUp({ content: returnMsg(checkResult, interaction.locale), flags: MessageFlags.Ephemeral })
if (interaction.options.getSubcommandGroup() == 'instances') { if (subcommandGroup == "instances") {
if (interaction.options.getSubcommand() == 'list') { if (subcommand == "list") {
let result = await AMP.ADSModule.GetInstances(host, sessionID) as InstanceResult 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 })
if (result.status === 'success') { await interaction.followUp({ content: t(interaction.locale, "amp.hosts_found", { count: result.data.length }) })
await interaction.followUp({ content: `${result.data.length} hôte(s) trouvé(s) !` }) await Promise.all(result.data.map(async host => {
result.data.forEach(async host => { const fields = [] as InstanceFields[]
let fields = [] as InstanceFields[]
host.AvailableInstances.forEach((instance: Instance) => { host.AvailableInstances.forEach((instance: Instance) => {
fields.push({ 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 })
name: instance.FriendlyName,
value: `**Running:** ${instance.Running}\n**Port:** ${instance.Port}\n**Module:** ${instance.Module}`,
inline: true
}) })
}) const embed = new EmbedBuilder()
let embed = new EmbedBuilder()
.setTitle(host.FriendlyName) .setTitle(host.FriendlyName)
.setDescription(`Liste des ${host.AvailableInstances.length} instances :`) .setDescription(t(interaction.locale, "amp.instance_list", { count: host.AvailableInstances.length }))
.setColor(interaction.guild?.members.me?.displayColor || '#ffc370') .setColor(interaction.guild?.members.me?.displayColor ?? "#ffc370")
.setTimestamp() .setTimestamp()
.setFields(fields) .setFields(fields)
return await interaction.followUp({ embeds: [embed] }) 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 })
})
}) })
}
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') { return interaction.respond(choices)
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))
}
}
}
} }

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 = { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("boost")
.setName('boost') .setDescription("Test the server boost")
.setDescription('Tester le boost du serveur !') .setDescriptionLocalizations({ fr: "Tester le boost du serveur" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator), .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é !`)
let guild = interaction.guild export async function execute(interaction: ChatInputCommandInteraction) {
if (!guild) return console.log(`\u001b[1;31m Aucun serveur trouvé !`) 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 const member = interaction.member
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`) if (!member) { logConsole('discordjs', 'boost.no_member'); return }
let boostRole = guild.roles.premiumSubscriberRole const guild = interaction.guild
if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) 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 !`) 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
}
let embed = new EmbedBuilder() const boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return }
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor) .setColor(guild.members.me.displayHexColor)
.setTitle(`Nouveau boost de ${member.user.username} !`) .setTitle(t(interaction.locale, "boost.new_boost_title", { username: member.user.username }))
.setDescription(` .setDescription(t(interaction.locale, "boost.new_boost_description", { count: (guild.premiumSubscriptionCount ?? 0).toString() }))
Merci à toi pour ce boost.\n
Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts !
`)
.setThumbnail(member.user.avatar) .setThumbnail(member.user.avatar)
.setTimestamp(new Date()) .setTimestamp(new Date())
await channel.send({ embeds: [embed] }) await channel.send({ embeds: [embed] })
await interaction.reply({ content: 'Va voir dans <#924353449930412153> !' }) return interaction.reply({ content: t(interaction.locale, "boost.check_channel", { channelId: "924353449930412153" }) })
}
} }

124
src/commands/global/database.ts Executable file → Normal file
View File

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

24
src/commands/global/ping.ts Executable file → Normal file
View File

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

40
src/commands/player/loop.ts Executable file → Normal file
View File

@@ -1,21 +1,29 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js' import { SlashCommandBuilder } from "discord.js"
import { useQueue } from'discord-player' import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import type { QueueRepeatMode } from "discord-player"
import { t } from "@/utils/i18n"
export default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("loop")
.setName('loop') .setDescription("Loop the current music")
.setDescription('Boucler la musique en cours de lecture.') .setNameLocalizations({ fr: "boucle" })
.addIntegerOption(option => option.setName('loop') .setDescriptionLocalizations({ fr: "Boucler la musique en cours de lecture" })
.setDescription('Mode de boucle (0 = Off, 1 = Titre, 2 = File d\'Attente; 3 = Autoplay)') .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) .setRequired(true)
.setMinValue(0) .setMinValue(0)
.setMaxValue(3)), .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) export async function execute(interaction: ChatInputCommandInteraction) {
return await interaction.reply(`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`) 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"))
} }

82
src/commands/player/lyrics.ts Executable file → Normal file
View File

@@ -1,45 +1,61 @@
import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js' import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"
import { useQueue } from 'discord-player' import type { ChatInputCommandInteraction } from "discord.js"
import { lyricsExtractor } from '@discord-player/extractor' import { useQueue, useMainPlayer } from "discord-player"
import type { LrcSearchResult } from "discord-player"
import { t } from "@/utils/i18n"
export default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("lyrics")
.setName('lyrics') .setDescription("Search for song lyrics")
.setDescription('Rechercher les paroles d\'une musique.') .setNameLocalizations({ fr: "paroles" })
.addStringOption(option => option.setName('recherche').setDescription('Chercher une musique spécifique')), .setDescriptionLocalizations({ fr: "Rechercher les paroles d'une musique" })
async execute(interaction: ChatInputCommandInteraction) { .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() await interaction.deferReply()
let query = interaction.options.getString('recherche', false) 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) { if (!query) {
let queue = useQueue(interaction.guild?.id ?? '') const queue = useQueue(interaction.guild?.id ?? "")
if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue"), flags: MessageFlags.Ephemeral })
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}` const track = queue.currentTrack
else query = track.title if (!track) return interaction.followUp({ content: t(interaction.locale, "player.no_current_track"), flags: MessageFlags.Ephemeral })
}
let lyricsFinder = lyricsExtractor() lyrics = await player.lyrics.search({ trackName: track.title, artistName: track.author })
let lyrics = await lyricsFinder.search(query).catch(() => null) if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral })
if (!lyrics) return interaction.followUp({ content: 'Pas de paroles trouvées !' }) const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997)
let trimmedLyrics = lyrics.lyrics.substring(0, 1997) embed
.setTitle(track.title)
let embed = new EmbedBuilder() .setURL(track.url)
.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) .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] }) return interaction.followUp({ embeds: [embed] })
} }
}

37
src/commands/player/panel.ts Executable file → Normal file
View File

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

24
src/commands/player/pause.ts Executable file → Normal file
View File

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

141
src/commands/player/play.ts Executable file → Normal file
View File

@@ -1,51 +1,41 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from 'discord.js' import { SlashCommandBuilder, MessageFlags } from "discord.js"
import { useMainPlayer, useQueue, QueryType } from 'discord-player' import type { ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from "discord.js"
import dbGuild from '../../schemas/guild' 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"
interface TrackSearchResult { name: string, value: string } 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 default { export async function execute(interaction: ChatInputCommandInteraction) {
data: new SlashCommandBuilder() const member = interaction.member as GuildMember
.setName('play') const voiceChannel = member.voice.channel
.setDescription('Jouer une musique.') if (!voiceChannel) return interaction.reply({ content: t(interaction.locale, "player.not_in_voice"), flags: MessageFlags.Ephemeral })
.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 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 })
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() await interaction.deferReply()
let query = interaction.options.getString('recherche', true) const query = interaction.options.getString("search", true)
let player = useMainPlayer() const player = useMainPlayer()
let queue = useQueue(interaction.guild?.id ?? '') let queue = useQueue(interaction.guild?.id ?? "")
if (!queue) { if (!queue) {
if (interaction.guild) queue = player.nodes.create(interaction.guild, { if (interaction.guild) queue = player.nodes.create(interaction.guild, {
@@ -63,34 +53,71 @@ export default {
}) })
else return else return
} }
try { if (!queue.connection) await queue.connect(voiceChannel) } try { if (!queue.connection) await queue.connect(voiceChannel) }
catch (error: unknown) { console.error(error) } 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 })
let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) const botId = interaction.client.user.id
if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`) const dbData = guildProfile.get("guildPlayer") as GuildPlayer
dbData.instances ??= []
let dbData = guildProfile.get('guildPlayer.replay') const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId)
dbData['textChannelId'] = interaction.channel?.id const instance = { botId, replay: {
dbData['voiceChannelId'] = voiceChannel.id textChannelId: interaction.channel?.id ?? "",
voiceChannelId: voiceChannel.id,
trackUrl: "",
progress: 0
} }
guildProfile.set('guildPlayer.replay', dbData) 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) 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]
let result = await player.search(query, { requestedBy: interaction.user }) const entry = queue.tasksQueue.acquire()
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() await entry.getTask()
queue.addTrack(track) queue.addTrack(track)
try { try {
if (!queue.isPlaying()) await queue.node.play() if (!queue.isPlaying()) await queue.node.play()
let track_source = track.source === 'youtube' ? 'Youtube' : track.source === 'spotify' ? 'Spotify' : 'Inconnu' startProgressSaving(queue.guild.id, botId)
return interaction.followUp(`Chargement de la musique **${track.title}** de **${track.author}** sur **${track_source}**...`) 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")
} catch (error: unknown) { console.error(error) } 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() } 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)
} }

25
src/commands/player/previous.ts Executable file → Normal file
View File

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

33
src/commands/player/queue.ts Executable file → Normal file
View File

@@ -1,19 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' import { SlashCommandBuilder, MessageFlags } from "discord.js"
import { useQueue } from 'discord-player' import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("queue")
.setName('queue') .setDescription("Get the queue")
.setDescription("Récupérer la file d'attente."), .setNameLocalizations({ fr: "file" })
async execute(interaction: ChatInputCommandInteraction) { .setDescriptionLocalizations({ fr: "Récupérer la file d'attente." })
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})` export async function execute(interaction: ChatInputCommandInteraction) {
let tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` }) const queue = useQueue(interaction.guild?.id ?? "")
if (tracks.length === 0) return interaction.reply({ content: `Lecture en cours : ${track} \nAucune musique dans la file d'attente.` }) 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})
return interaction.reply({ content: `Lecture en cours : ${track} \nFile d'attente actuelle : \n${tracks.join('\n')}` }) 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") }) })
} }

25
src/commands/player/resume.ts Executable file → Normal file
View File

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

25
src/commands/player/shuffle.ts Executable file → Normal file
View File

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

25
src/commands/player/skip.ts Executable file → Normal file
View File

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

28
src/commands/player/stop.ts Executable file → Normal file
View File

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

39
src/commands/player/volume.ts Executable file → Normal file
View File

@@ -1,21 +1,26 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' import { SlashCommandBuilder, MessageFlags } from "discord.js"
import { useQueue } from 'discord-player' import type { ChatInputCommandInteraction } from "discord.js"
import { useQueue } from "discord-player"
import { t } from "@/utils/i18n"
export default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("volume")
.setName('volume') .setDescription("Change the music volume")
.setDescription('Modifie le volume de la musique.') .setDescriptionLocalizations({ fr: "Modifie le volume de la musique" })
.addIntegerOption(option => option.setName('volume') .addIntegerOption(option => option
.setDescription('Le volume à mettre (%)') .setName("volume")
.setDescription("The volume to set (%)")
.setDescriptionLocalizations({ fr: "Le volume à mettre (%)" })
.setRequired(true) .setRequired(true)
.setMinValue(1) .setMinValue(0)
.setMaxValue(100)), .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) export async function execute(interaction: ChatInputCommandInteraction) {
return await interaction.reply(`Volume modifié à ${volume}% !`) 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() }))
} }

90
src/commands/salonpostam/crack.ts Executable file → Normal file
View File

@@ -1,58 +1,70 @@
import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, MessageReaction, User }from 'discord.js' import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"
import * as crack from '../../utils/crack' 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 default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder().setName('crack').setDescription('Télécharge un crack sur le site online-fix.me !') .setName("crack")
.addStringOption(option => option.setName('jeu').setDescription('Quel jeu tu veux DL ?').setRequired(true)), .setDescription("Download a crack from online-fix.me")
async execute(interaction: ChatInputCommandInteraction) { .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() await interaction.deferReply()
let query = interaction.options.getString('jeu') const query = interaction.options.getString("game", true)
if (!query) return 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 games = await crack.search(query) as crack.Game[] let game = {} as CrackGame
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) { if (games.length > 1) {
games = games.slice(0, 9) 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⃣'] 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]) for (let i = 0; i < games.length; i++) await message.react(emojis[i])
// Wait for a reaction to be added by the interaction author. // 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 } const filter = (reaction: MessageReaction, user: User) => {
await message.awaitReactions({ filter, max: 1, time: 5000, errors: ['time'] }).then(collected => { if (reaction.emoji.name) return (emojis.includes(reaction.emoji.name) && user.id === interaction.user.id)
console.log(collected) return false
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] 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 ?? "")
let url = await crack.repo(game) 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 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() const file = await torrent(url)
.setColor('#ffc370') 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) .setTitle(game.name)
.setURL(game.link) .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 !`) .setDescription(t(interaction.locale, "salonpostam.crack.game_found", { query, link: `https://angels-dev.fr/magnet/${link}` }))
await interaction.followUp({ embeds: [embed], files: [filePath] }) 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[]

42
src/commands/salonpostam/papa.ts Executable file → Normal file
View File

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

81
src/commands/salonpostam/parle.ts Executable file → Normal file
View File

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

56
src/commands/salonpostam/spam.ts Executable file → Normal file
View File

@@ -1,29 +1,47 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' import { SlashCommandBuilder, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export default { export const data = new SlashCommandBuilder()
data: new SlashCommandBuilder() .setName("spam")
.setName('spam') .setDescription("Spam a user with a message")
.setDescription('Spam') .setDescriptionLocalizations({ fr: "Spammer un utilisateur avec un message" })
.addUserOption(option => option.setName('user').setDescription('Spam').setRequired(true)) .addUserOption(option => option
.addStringOption(option => option.setName('string').setDescription('Spam').setRequired(true)) .setName("user")
.addIntegerOption(option => option.setName('integer').setDescription('Spam').setRequired(true)), .setDescription("Target user")
async execute(interaction: ChatInputCommandInteraction) { .setNameLocalizations({ fr: "utilisateur" })
let user = interaction.options.getUser('user') .setDescriptionLocalizations({ fr: "Utilisateur cible" })
let string = interaction.options.getString('string') .setRequired(true)
let integer = interaction.options.getInteger('integer') )
.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)
)
await interaction.reply({ content: 'Spam', ephemeral: true }) 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 let i = 0
function myLoop() { function myLoop() {
setTimeout(function () { setTimeout(() => {
if (!user) return user.send(string).catch(console.error)
if (!string) return
if (!integer) return
user.send(string).catch(error => console.error(error))
i++ i++
if (i < integer) myLoop() if (i < integer) myLoop()
}, 1000) }, 1000)
} }
myLoop() myLoop()
} }
}

29
src/commands/salonpostam/update.ts Executable file → Normal file
View File

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

11
src/events/client/error.ts Executable file → Normal file
View File

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

18
src/events/client/guildCreate.ts Executable file → Normal file
View File

@@ -1,14 +1,12 @@
import { Events, Guild } from 'discord.js' import { Events, Guild } from "discord.js"
import dbGuildInit from '../../utils/dbGuildInit' import dbGuildInit from "@/utils/dbGuildInit"
import { logConsole } from "@/utils/console"
export default { export const name = Events.GuildCreate
name: Events.GuildCreate, export async function execute(guild: Guild) {
async execute(guild: Guild) { logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() })
console.log(`Joined "${guild.name}" with ${guild.memberCount} members`)
let guildProfile = await dbGuildInit(guild) const 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 !`) logConsole('mongoose', 'guild_create', { name: guildProfile.guildName })
}
} }

56
src/events/client/guildMemberAdd.ts Executable file → Normal file
View File

@@ -1,44 +1,42 @@
import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' import { Events, EmbedBuilder, ChannelType } from "discord.js"
import type { GuildMember } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
export default { export const name = Events.GuildMemberAdd
name: Events.GuildMemberAdd, export async function execute(member: GuildMember) {
async execute(member: GuildMember) { if (member.guild.id === "1086577543651524699") {
if (member.guild.id === '1086577543651524699') { // Salon posé tamisé // Salon posé tamisé
let guild = member.guild const guild = member.guild
guild.members.fetch().then(() => { guild.members.fetch().then(async () => {
let i = 0 let i = 0
guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) guild.members.cache.forEach(member => { if (!member.user.bot) i++ })
let channel = guild.channels.cache.get('1091140609139560508') const channel = guild.channels.cache.get("1091140609139560508")
if (!channel) return if (!channel) return
channel.setName('Changement...') await channel.setName("Changement...")
channel.setName(`${i} Gens Posés`) await channel.setName(`${i} Gens Posés`)
}).catch(console.error) }).catch(console.error)
} else if (member.guild.id === '796327643783626782') { // Jujul Community } else if (member.guild.id === "796327643783626782") {
let guild = member.guild // Jujul Community
const guild = member.guild
if (!guild.members.me) return
let channel = guild.channels.cache.get('837248593609097237') as TextChannel const channel = guild.channels.cache.get("837248593609097237")
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "837248593609097237" !`) if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'guild_member_add', { channelId: '837248593609097237' })
return
}
if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) const embed = new EmbedBuilder()
let embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor) .setColor(guild.members.me.displayHexColor)
.setTitle(`Salut ${member.user.username} !`) .setTitle(t(guild.preferredLocale, "welcome.title", { username: member.user.username }))
.setDescription(` .setDescription(t(guild.preferredLocale, "welcome.description", { memberCount: guild.memberCount.toString() }))
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()) .setThumbnail(member.user.avatarURL())
.setTimestamp(new Date()) .setTimestamp(new Date())
await channel.send({ embeds: [embed] }) return channel.send({ embeds: [embed] })
}
} }
} }

25
src/events/client/guildMemberRemove.ts Executable file → Normal file
View File

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

View File

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

18
src/events/client/guildUpdate.ts Executable file → Normal file
View File

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

62
src/events/client/interactionCreate.ts Executable file → Normal file
View File

@@ -1,45 +1,49 @@
import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction } from 'discord.js' import { Events } from "discord.js"
import { playerButtons, playerEdit } from '../../utils/player' import type { Interaction } from "discord.js"
import commands from "@/commands"
export default { import buttons, { buttonFolders } from "@/buttons"
name: Events.InteractionCreate, import selectMenus from "@/selectmenus"
async execute(interaction: Interaction) { import { playerEdit } from "@/utils/player"
//if (!interaction.isAutocomplete() && !interaction.isChatInputCommand() && !interaction.isButton()) return console.error(`Interaction ${interaction.commandName} is not a command.`) import { logConsole, logConsoleError } from "@/utils/console"
export const name = Events.InteractionCreate
export async function execute(interaction: Interaction) {
if (interaction.isChatInputCommand()) { if (interaction.isChatInputCommand()) {
interaction = interaction as ChatInputCommandInteraction const chatInputCommand = commands.find(cmd => cmd.data.name == interaction.commandName)
if (!chatInputCommand) { logConsole('discordjs', 'interaction_create.command_not_found', { command: interaction.commandName }); return }
let chatInputCommand = interaction.client.commands.get(interaction.commandName) logConsole('discordjs', 'interaction_create.command_launched', { command: interaction.commandName, user: interaction.user.tag })
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) } try { await chatInputCommand.execute(interaction) }
catch (error) { console.error(`Error executing ${interaction.commandName}:`, error) } catch (error) { logConsoleError('discordjs', 'interaction_create.command_error', { command: interaction.commandName }, error as Error) }
} }
else if (interaction.isAutocomplete()) { else if (interaction.isAutocomplete()) {
interaction = interaction as AutocompleteInteraction const autocompleteRun = commands.find(cmd => cmd.data.name == interaction.commandName)
if (!autocompleteRun?.autocompleteRun) { logConsole('discordjs', 'interaction_create.autocomplete_not_found', { command: interaction.commandName }); return }
let autoCompleteRun = interaction.client.commands.get(interaction.commandName) logConsole('discordjs', 'interaction_create.autocomplete_launched', { command: interaction.commandName, user: interaction.user.tag })
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) { logConsoleError('discordjs', 'interaction_create.autocomplete_error', { command: interaction.commandName }, error as Error) }
try { await autoCompleteRun.autocompleteRun(interaction) }
catch (error) { console.error(`Error autocompleting ${interaction.commandName}:`, error) }
} }
else if (interaction.isButton()) { else if (interaction.isButton()) {
interaction = interaction as ButtonInteraction const button = buttons.find(btn => btn.id === interaction.customId)
if (!button) { logConsole('discordjs', 'interaction_create.button_not_found', { id: interaction.customId }); return }
let button = interaction.client.buttons.get(interaction.customId) logConsole('discordjs', 'interaction_create.button_clicked', { id: interaction.customId, user: interaction.user.tag })
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) } try { await button.execute(interaction) }
catch (error) { console.error(`Error clicking ${interaction.customId}:`, error) } 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) }
} }
} }

212
src/events/client/ready.ts Executable file → Normal file
View File

@@ -1,117 +1,139 @@
import { Events, Client, ActivityType } from 'discord.js' import { Events, ActivityType, ChannelType } from "discord.js"
import { SpotifyExtractor } from '@discord-player/extractor' import type { Client } from "discord.js"
import { YoutubeiExtractor } from 'discord-player-youtubei' import { useMainPlayer } from "discord-player"
import { useMainPlayer } from 'discord-player' import { SpotifyExtractor } from "@discord-player/extractor"
import { connect } from 'mongoose' import { YoutubeiExtractor } from "discord-player-youtubei"
import WebSocket from 'websocket' import { connect } from "mongoose"
import chalk from 'chalk' import type { Document } from "mongoose"
import 'dotenv/config' 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"
import dbGuildInit from '../../utils/dbGuildInit' export const name = Events.ClientReady
import dbGuild from '../../schemas/guild' export const once = true
import { playerDisco, playerReplay } from '../../utils/player' export async function execute(client: Client) {
import * as Twitch from '../../utils/twitch' logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" })
import rss from '../../utils/rss' client.user?.setActivity("some bangers...", { type: ActivityType.Listening })
export default { await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error)
name: Events.ClientReady, await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error)
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) const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}`
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) await connect(mongo_url).catch(console.error)
if (process.env.NODE_ENV === "development") await twitchClient.eventSub.deleteAllSubscriptions()
const streamerIds: string[] = []
let guilds = client.guilds.cache await Promise.all(client.guilds.cache.map(async guild => {
guilds.forEach(async guild => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id }) let guildProfile = await dbGuild.findOne({ guildId: guild.id })
guildProfile ??= await dbGuildInit(guild)
if (!guildProfile) guildProfile = await dbGuildInit(guild) const dbDataPlayer = guildProfile.get("guildPlayer") as GuildPlayer
if (guildProfile.guildPlayer?.replay?.enabled && guildProfile.guildPlayer?.replay?.textChannelId) await playerReplay(client, guildProfile) 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 } client.disco = { interval: {} as NodeJS.Timeout }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
client.disco.interval = setInterval(async () => { client.disco.interval = setInterval(async () => {
let guildProfile = await dbGuild.findOne({ guildId: guild.id }) const guildProfile = await dbGuild.findOne({ guildId: guild.id })
const dbDataDisco = guildProfile?.get("guildPlayer.disco") as Disco
if (guildProfile?.guildPlayer?.disco?.enabled) { if (dbDataDisco.enabled) {
let state = await playerDisco(client, guildProfile) const state = await playerDisco(client, guild, dbDataDisco)
if (state === 'clear') clearInterval(client.disco.interval) if (state === "clear") clearInterval(client.disco.interval)
} }
}, 3000) }, 3000)
client.rss = { interval: {} as NodeJS.Timeout } // Gestion du timer LCD Freebox
client.rss.interval = setInterval(async () => { const dbDataFbx = guildProfile.get("guildFbx") as GuildFbx
let guildProfile = await dbGuild.findOne({ guildId: guild.id }) if (dbDataFbx.enabled && dbDataFbx.lcd) {
if (dbDataFbx.lcd.enabled && dbDataFbx.lcd.botId === client.user?.id) {
if (guildProfile?.guildRss?.enabled) { logConsole('freebox', 'lcd_timer_restored', { guild: guild.name })
let state = await rss(client, guildProfile) Freebox.Timer.schedule(client, guild.id, dbDataFbx)
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 const dbDataTwitch = guildProfile.get("guildTwitch") as GuildTwitch
else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild) if (!dbDataTwitch.enabled) return
if (!dbDataTwitch.streamers.length) { logConsole('twitch', 'ready.no_streamers_configured', { guild: guild.name }); return }
} catch (error) { console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)) } } }) await Promise.all(dbDataTwitch.streamers.map(async streamer => {
.on('error', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error))) if (streamerIds.includes(streamer.twitchUserId)) return
.on('close', () => { streamerIds.push(streamer.twitchUserId)
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') 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 { export const name = "connected"
name: 'connected', export function execute() {
async execute() { logConsole('mongoose', 'connected')
console.log(chalk.green('[Mongoose] Connected to MongoDB !'))
}
} }

View File

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

View File

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

View File

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

15
src/events/player/audioTrackAdd.ts Executable file → Normal file
View File

@@ -1,10 +1,11 @@
import { GuildQueue, Track } from 'discord-player' import type { GuildQueue, Track } from "discord-player"
import { PlayerMetadata } from '../../utils/player' import type { PlayerMetadata } from "@/types/player"
import { t } from "@/utils/i18n"
export default { export const name = "audioTrackAdd"
name: 'audioTrackAdd', export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) {
async execute(queue: GuildQueue<PlayerMetadata>, track: Track) {
// Emitted when the player adds a single song to its queue // 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 !`) 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 }) })
} }

14
src/events/player/audioTracksAdd.ts Executable file → Normal file
View File

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

11
src/events/player/debug.ts Executable file → Normal file
View File

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

29
src/events/player/disconnect.ts Executable file → Normal file
View File

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

17
src/events/player/emptyChannel.ts Executable file → Normal file
View File

@@ -1,11 +1,14 @@
import { GuildQueue } from 'discord-player' import type { GuildQueue } from "discord-player"
import { PlayerMetadata } from '../../utils/player' import type { PlayerMetadata } from "@/types/player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export default { export const name = "emptyChannel"
name: 'emptyChannel', export async function execute(queue: GuildQueue<PlayerMetadata>) {
async execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the voice channel has been empty for the set threshold // Emitted when the voice channel has been empty for the set threshold
// Bot will automatically leave the voice channel with this event // 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.`) 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") })
} }

17
src/events/player/emptyQueue.ts Executable file → Normal file
View File

@@ -1,10 +1,13 @@
import { GuildQueue } from 'discord-player' import type { GuildQueue } from "discord-player"
import { PlayerMetadata } from '../../utils/player' import type { PlayerMetadata } from "@/types/player"
import { stopProgressSaving } from "@/utils/player"
import { t } from "@/utils/i18n"
export default { export const name = "emptyQueue"
name: 'emptyQueue', export async function execute(queue: GuildQueue<PlayerMetadata>) {
async execute(queue: GuildQueue<PlayerMetadata>) {
// Emitted when the player queue has finished // Emitted when the player queue has finished
queue.metadata.channel.send("File d'attente vide !") 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