Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
29bb1a00fb Bump cross-spawn from 7.0.3 to 7.0.6
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-21 01:50:22 +00:00
137 changed files with 7203 additions and 13044 deletions

12
.dockerignore Executable file
View File

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

View File

@@ -1,29 +0,0 @@
# 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

@@ -1,79 +0,0 @@
name: Build and Push Docker Image
on:
push:
branches:
- master
tags:
- 'build_*'
pull_request:
branches:
- master
env:
REGISTRY: rgy.angels-dev.fr
IMAGE_PATH: prod
IMAGE_NAME: bot_tamiseur
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
run: npm run build --if-present
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }}
tags: |
# Tag avec le nom du tag Git
type=ref,event=tag
# Tag 'latest' pour la branche master
type=raw,value=latest,enable={{is_default_branch}}
# Tag avec le SHA pour les autres branches
type=sha,prefix=sha-
labels: |
org.opencontainers.image.title=${{ env.IMAGE_NAME }}
org.opencontainers.image.description=Bot Discord de moi
org.opencontainers.image.url=https://git.zac.ovh/zachary/bot_Tamiseur
org.opencontainers.image.source=https://git.zac.ovh/zachary/bot_Tamiseur
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created={{date 'RFC3339'}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: build/node.dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,22 +0,0 @@
# 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,4 +1,5 @@
.env
dist/ dist/
node_modules/ node_modules/
public/cracks/ public/cracks/*
.env*
.ncurc.json

18
.vscode/launch.json vendored Executable file
View File

@@ -0,0 +1,18 @@
{
// 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
}
]
}

3
.vscode/settings.json vendored Executable file
View File

@@ -0,0 +1,3 @@
{
"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"
}

19
Dockerfile Executable file
View File

@@ -0,0 +1,19 @@
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

@@ -1,21 +0,0 @@
# =====================
# Build new application
# =====================
.PHONY: tag-build
tag-build: ## DEV : Build a prod version with a timestamped tag
@export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \
echo TAG = $$TIMESTAMP; \
git tag $$TIMESTAMP; \
git push origin $$TIMESTAMP;
# ===============
# Deployment tags
# ===============
.PHONY: tag-deploy
tag-deploy: ## DEV : Set tag to current HEAD to deploy in production
@export TIMESTAMP=deploy_`date +"%G-%m-%d_%Hh%M"`; \
echo TAG = $$TIMESTAMP; \
git tag $$TIMESTAMP; \
git push origin $$TIMESTAMP;

128
README.md Normal file → Executable file
View File

@@ -1,129 +1,3 @@
# Bot Tamiseur # Discord
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

View File

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

View File

@@ -1,12 +0,0 @@
# Version schéma helm (v2 = helm3)
apiVersion: v2
# Nom de l'application déployée
name: bot_tamiseur
# Version du chart : doit changer si l'application change ou si la configuration du chart change
#version: 1
version: "1"
# icon (optionnel) mais génère un warning avec "helm lint"
icon: https://helm.sh/img/helm-logo.svg

View File

@@ -1,34 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: 1
revisionHistoryLimit: 0
strategy:
type: {{ .Values.deployment.strategy }}
selector:
matchLabels:
pod: {{ .Release.Name }}
template:
metadata:
labels:
pod: {{ .Release.Name }}
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: {{ .Release.Name }}
image: "{{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag }}"
imagePullPolicy: {{ .Values.deployment.image.pullPolicy }}
env:
{{ range $envName, $envValue := .Values.deployment.env }}
- name: {{ $envName | quote}}
value: {{ $envValue | quote}}
{{ end }}
{{- if .Values.deployment.resources.enable }}
resources:
{{- toYaml .Values.deployment.resources | nindent 12 }}
{{- end }}

View File

@@ -1,18 +0,0 @@
deployment:
replica: 1
strategy: RollingUpdate
image:
repository: "rgy.angels-dev.fr/prod/bot_tamiseur"
tag: "build_2025-06-10_01h49"
pullPolicy: IfNotPresent
env:
NODE_ENV: "production"
## Pas de limite CPU pour éviter latence
resources:
limits:
# cpu: ""
# Memory: "500Mi"
requests:
Cpu: "0.1"
Memory: "50Mi"

View File

@@ -1,103 +0,0 @@
# 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,22 +1,23 @@
import eslint from "@eslint/js" import typescriptEslint from "@typescript-eslint/eslint-plugin"
import tseslint from "typescript-eslint" import tsParser from "@typescript-eslint/parser"
import { FlatCompat } from "@eslint/eslintrc"
import { fileURLToPath } from "node:url"
import path from "node:path"
import js from "@eslint/js"
export default tseslint.config( const __filename = fileURLToPath(import.meta.url)
eslint.configs.recommended, const __dirname = path.dirname(__filename)
tseslint.configs.recommendedTypeChecked, const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
})
export default [
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
{ {
languageOptions: { plugins: { "@typescript-eslint": typescriptEslint },
parserOptions: { languageOptions: { parser: tsParser },
projectService: true, rules: { "prefer-const": "off" }
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"]
} }
) ]

11343
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,57 @@
{ {
"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": "4.0.0", "version": "3.0.3",
"author": { "author": {
"name": "Zachary Guénot" "name": "Zachary Guénot"
}, },
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"start": "node index.js", "format": "prettier --write .",
"start:prod": "NODE_ENV=production node dist/index.js", "start": "ts-node src/index.ts",
"start:dev": "NODE_ENV=development tsx src/index.ts", "dev": "nodemon -e ts src/index.ts",
"dev": "NODE_ENV=development tsx watch src/index.ts", "build": "tsc",
"lint": "eslint .", "lint": "eslint src/**/*.ts",
"lint:fix": "eslint . --fix", "prod": "node dist/index.js"
"build": "tsup",
"updateall": "ncu -u && npm i"
}, },
"//": [ "//": [
"Garder parse-torrent à la version 9.1.5 pour éviter un bug exports avec la version >=10.0.0" "Garder chalk à la version 4.1.2 pour éviter un bug ESM avec la version >=5.0.0"
], ],
"dependencies": { "dependencies": {
"@discord-player/extractor": "^7.1.0", "@discord-player/equalizer": "^0.2.3",
"@discordjs/voice": "^0.18.0", "@discord-player/extractor": "^4.5.0",
"@twurple/api": "^7.3.0", "@discordjs/voice": "^0.17.0",
"@twurple/auth": "^7.3.0", "@evan/opus": "^1.0.3",
"@twurple/eventsub-http": "^7.3.0", "axios": "^1.7.4",
"@twurple/eventsub-ngrok": "^7.3.0", "bufferutil": "^4.0.8",
"axios": "^1.9.0", "chalk": "^4.1.2",
"bufferutil": "^4.0.9", "discord-player": "^6.7.1",
"chalk": "^5.4.1", "discord-player-youtubei": "^1.2.4",
"discord-player": "^7.1.0", "discord.js": "^14.15.3",
"discord-player-youtubei": "^1.4.6", "dotenv": "^16.4.5",
"discord.js": "^14.19.3",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"mediaplex": "^1.0.0", "jsdom": "^24.1.1",
"mongoose": "^8.15.1", "libsodium-wrappers": "^0.7.14",
"mediaplex": "^0.0.9",
"mongoose": "^8.5.2",
"parse-torrent": "^9.1.5", "parse-torrent": "^9.1.5",
"zlib-sync": "^0.1.10" "require-all": "^3.0.0",
"rss-parser": "^3.13.0",
"ts-node": "^10.9.2",
"utf-8-validate": "^6.0.4",
"websocket": "^1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.28.0", "@eslint/eslintrc": "^3.1.0",
"@types/node": "^22.15.30", "@eslint/js": "^9.8.0",
"@swc/core": "^1.7.6",
"@types/node": "^22.1.0",
"@types/parse-torrent": "^5.8.7", "@types/parse-torrent": "^5.8.7",
"dotenv": "^16.5.0", "@types/websocket": "^1.0.10",
"eslint": "^9.28.0", "@typescript-eslint/eslint-plugin": "^8.0.1",
"tsup": "^8.5.0", "@typescript-eslint/parser": "^8.0.1",
"tsx": "^4.19.4", "eslint": "^9.8.0",
"typescript": "^5.8.3", "nodemon": "^3.1.4",
"typescript-eslint": "^8.33.1" "prettier": "^3.3.3"
} }
} }

View File

@@ -1,11 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,76 +0,0 @@
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

@@ -1,71 +0,0 @@
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 })
}
}

View File

@@ -1,26 +0,0 @@
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[]

16
src/buttons/loop.ts Executable file
View File

@@ -0,0 +1,16 @@
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 })
}
}

15
src/buttons/pause.ts Executable file
View File

@@ -0,0 +1,15 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,14 +0,0 @@
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 })
}

15
src/buttons/previous.ts Executable file
View File

@@ -0,0 +1,15 @@
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 })
}
}

15
src/buttons/resume.ts Executable file
View File

@@ -0,0 +1,15 @@
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 })
}
}

15
src/buttons/shuffle.ts Executable file
View File

@@ -0,0 +1,15 @@
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 })
}
}

15
src/buttons/skip.ts Executable file
View File

@@ -0,0 +1,15 @@
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 })
}
}

15
src/buttons/stop.ts Executable file
View File

@@ -0,0 +1,15 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,51 +0,0 @@
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

@@ -1,46 +0,0 @@
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 })
}

16
src/buttons/volume_down.ts Executable file
View File

@@ -0,0 +1,16 @@
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 })
}
}

16
src/buttons/volume_up.ts Executable file
View File

@@ -0,0 +1,16 @@
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 })
}
}

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

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

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

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

@@ -1,353 +0,0 @@
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

@@ -1,17 +0,0 @@
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[]

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

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

@@ -1,198 +0,0 @@
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)
}

View File

@@ -1,26 +0,0 @@
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

@@ -1,60 +0,0 @@
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

@@ -1,31 +0,0 @@
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[]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,151 @@
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

@@ -1,15 +0,0 @@
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[]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

View File

@@ -1,21 +0,0 @@
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[]

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

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

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

@@ -1,139 +1,111 @@
import { Events, ActivityType, ChannelType } from "discord.js" import { Events, Client, ActivityType } from 'discord.js'
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.loadDefault(ext => ext === '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
} let client_id = process.env.TWITCH_APP_ID as string
} let client_secret = process.env.TWITCH_APP_SECRET as string
const dbDataTwitch = guildProfile.get("guildTwitch") as GuildTwitch let twitch = new WebSocket.client().on('connect', async connection => {
if (!dbDataTwitch.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub WebSocket Connected !`))
if (!dbDataTwitch.streamers.length) { logConsole('twitch', 'ready.no_streamers_configured', { guild: guild.name }); return }
connection.on('message', async message => { if (message.type === 'utf8') { try {
await Promise.all(dbDataTwitch.streamers.map(async streamer => { let data = JSON.parse(message.utf8Data)
if (streamerIds.includes(streamer.twitchUserId)) return let channel_access_token = guildProfile.get('guildTwitch')?.channelAccessToken as string
streamerIds.push(streamer.twitchUserId)
// Check when Twitch asks to login
const user = await twitchClient.users.getUserById(streamer.twitchUserId) if (data.metadata.message_type === 'session_welcome') {
if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return }
// Check if the channel access token is still valid before connecting
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId) channel_access_token = await Twitch.checkChannel(client_id, client_secret, channel_access_token, guild) as string
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) { if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`))
// eslint-disable-next-line @typescript-eslint/no-misused-promises
listener.onStreamOnline(streamer.twitchUserId, onlineSub) // Get broadcaster user id and reward id
logConsole('twitch', 'listener_registered', { type: 'stream.online', name: user.name, id: streamer.twitchUserId }) let broadcaster_user_id = await Twitch.getUserInfo(client_id, channel_access_token, 'id') as string
}
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) { let topics: { [key: string]: { version: string; condition: { broadcaster_user_id: string } } } = {
// eslint-disable-next-line @typescript-eslint/no-misused-promises 'stream.online': { version: '1', condition: { broadcaster_user_id } },
listener.onStreamOffline(streamer.twitchUserId, offlineSub) 'stream.offline': { version: '1', condition: { broadcaster_user_id } }
logConsole('twitch', 'listener_registered', { type: 'stream.offline', name: user.name, id: streamer.twitchUserId }) }
}
// Subscribe to all events required
logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) for (let type in topics) {
console.log(chalk.magenta(`[Twitch] {${guild.name}} Creating ${type}...`))
const stream = await user.getStream() let { version, condition } = topics[type]
if (stream && streamer.messageId) {
logConsole('twitch', 'ready.stream_restoration', { guild: guild.name, userName: user.name, userId: streamer.twitchUserId }) let status = await Twitch.subscribeToEvents(client_id, channel_access_token, data.payload.session.id, type, version, condition)
if (!status) return console.error(chalk.magenta(`[Twitch] {${guild.name}} Failed to create ${type}`))
// Vérifier que le message existe encore else if (status.error) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Erreur de connexion EventSub, veuillez vous reconnecter !`))
if (!dbDataTwitch.channelId) return else console.log(chalk.magenta(`[Twitch] {${guild.name}} Successfully created ${type}`))
const channel = await guild.channels.fetch(dbDataTwitch.channelId) }
if (channel && (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement)) { }
try {
await channel.messages.fetch(streamer.messageId) // Handle notification messages
startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild)
logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name })
} catch (error) { } catch (error) { console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)) } } })
logConsole('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }) .on('error', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)))
console.error(error) .on('close', () => {
await cleanupMessageId(guildProfile, streamer.twitchUserId) console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub Connection Closed !`))
} twitch.connect('wss://eventsub.wss.twitch.tv/ws')
} })
} else if (streamer.messageId) { }).on('connectFailed', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)))
// 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 }) let dbData = guildProfile.get('guildTwitch')
await cleanupMessageId(guildProfile, streamer.twitchUserId) if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled for "${guild?.name}", please activate with \`/database edit guildTwitch.enabled True\` !`))
} else if (!client_id || !client_secret) return console.log(chalk.magenta(`[Twitch] {${guild.name}} App ID or Secret is not defined !`))
else twitch.connect('wss://eventsub.wss.twitch.tv/ws')
logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) })
})) }
})) }
const subs = await twitchClient.eventSub.getSubscriptions()
await Promise.all(subs.data.map(async sub => {
if (streamerIds.includes(sub.condition.broadcaster_user_id as string)) return
if (sub.type !== "stream.online" && sub.type !== "stream.offline") return
await sub.unsubscribe().catch(console.error)
logConsole('twitch', 'unsubscribed', { type: sub.type, id: sub.condition.broadcaster_user_id as string })
}))
}
async function cleanupMessageId(guildProfile: Document, twitchUserId: string) {
try {
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === twitchUserId)
if (streamerIndex === -1) return
dbData.streamers[streamerIndex].messageId = ""
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save()
} catch (error) {
logConsole('twitch', 'ready.cleanup_error', { userId: twitchUserId })
console.error(error)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
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[]

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

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

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

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

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

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

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

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

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