Compare commits

...

2 Commits

Author SHA1 Message Date
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
133 changed files with 8097 additions and 4337 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,20 +46,14 @@ 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
type=sha,prefix={{branch}}-
# Tag latest pour la branche master
type=raw,value=latest,enable={{is_default_branch}}
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
@@ -61,7 +65,7 @@ jobs:
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 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 }}
@@ -71,6 +75,6 @@ jobs:
- name: Generate artifact attestation - name: Generate artifact attestation
uses: actions/attest-build-provenance@v1 uses: actions/attest-build-provenance@v1
with: with:
subject-name: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }} subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true 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
View File

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

24
build/node.dockerfile Normal file
View File

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

View File

@@ -3,7 +3,7 @@ deployment:
strategy: RollingUpdate 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 })
}
}

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

View File

@@ -1,37 +1,39 @@
import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, TextChannel, PermissionFlagsBits } from 'discord.js' import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
module.exports = { 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)) {
let embed = new EmbedBuilder() logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" })
.setColor(guild.members.me.displayHexColor) return
.setTitle(`Nouveau boost de ${member.user.username} !`)
.setDescription(`
Merci à toi pour ce boost.\n
Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts !
`)
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
await interaction.reply({ content: 'Va voir dans <#924353449930412153> !' })
} }
}
const boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return }
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(t(interaction.locale, "boost.new_boost_title", { username: member.user.username }))
.setDescription(t(interaction.locale, "boost.new_boost_description", { count: (guild.premiumSubscriptionCount ?? 0).toString() }))
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
return interaction.reply({ content: t(interaction.locale, "boost.check_channel", { channelId: "924353449930412153" }) })
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { Events } from 'discord.js' import { Events } from "discord.js"
import { logConsoleError } from "@/utils/console"
export default { 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)
}
} }

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 {
name: Events.GuildCreate, export const name = Events.GuildCreate
async execute(guild: Guild) { export async function execute(guild: Guild) {
console.log(`Joined "${guild.name}" with ${guild.memberCount} members`) logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() })
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}" !`)
logConsole('mongoose', 'guild_create', { name: guildProfile.guildName })
console.log(`Database data for new guild "${guildProfile.guildName}" successfully initialized !`) }
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
name: 'audioTrackAdd', export const name = "audioTrackAdd"
async execute(queue: GuildQueue<PlayerMetadata>, track: Track) { export async function 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 }) })
}

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 {
name: 'audioTracksAdd', export const name = "audioTracksAdd"
async execute(queue: GuildQueue<PlayerMetadata>, track: Array<Track>) { export async function execute(queue: GuildQueue<PlayerMetadata>, track: 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() }) })
} }

View File

@@ -1,10 +1,9 @@
import { GuildQueue } from 'discord-player' import type { GuildQueue } from "discord-player"
import { logConsoleDev } from "@/utils/console"
export default {
name: 'debug', export const name = "debug"
async execute(queue: GuildQueue, message: string) { export function 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 })
} }
}

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 {
name: 'disconnect', export const name = "disconnect"
async execute(queue: GuildQueue<PlayerMetadata>) { export async function 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)
}
}

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

View File

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

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