Compare commits

...

9 Commits

Author SHA1 Message Date
f28c181e8a 🐛 Fix Permission 2025-01-04 22:10:44 +01:00
1cce7f01b7 Ajout du système d'avertissement 2025-01-04 22:08:38 +01:00
2f020dc5a7 Ajout Repeat all/one, Shuffle Queue 2024-12-26 22:38:52 +01:00
c7b07a5843 Ajout de la commande /nowplaying, /pause, /volume 2024-12-26 22:19:28 +01:00
e2d61d644a Refacto des commandes + listeners + ajout de la commande queue et de la commande skip 2024-12-25 10:53:08 +01:00
Melaine Gérard
f58669e522
🐛 fix playlists loading only first audio 2024-12-24 15:16:57 +01:00
Melaine Gérard
c112781ee6
🐛 fix queue 2024-12-24 15:03:04 +01:00
Melaine Gérard
7fce86174f
🐛 Fix Stage Channel 2024-12-24 14:38:02 +01:00
Melaine Gérard
92b762fdb0
🐛 Tout est patché 2024-12-24 14:24:06 +01:00
33 changed files with 1366 additions and 97 deletions

@ -1,4 +1,8 @@
BOT_TOKEN=
GUILD_ID=
DEFAULT_ROLE_ID=
ROLE_ID=
ROLE_ID=
LOG_CHANNEL_ID=
DB_URL=jdbc:postgresql://localhost:5434/kiss_shot_acerola
DB_USER=postgres
DB_PASSWORD=kiss_shot_acerola

@ -17,10 +17,14 @@ repositories {
}
dependencies {
implementation("org.hibernate:hibernate-core:6.6.2.Final")
implementation("org.hibernate:hibernate-hikaricp:6.6.2.Final")
implementation("org.postgresql:postgresql:42.7.4")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
implementation("net.dv8tion:JDA:5.2.1")
implementation("ch.qos.logback:logback-classic:1.5.12")
implementation ("dev.arbjerg:lavaplayer:2.2.2")
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
}

13
docker-compose.yml Normal file

@ -0,0 +1,13 @@
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: kiss_shot_acerola
POSTGRES_DB: kiss_shot_acerola
ports:
- "5434:5432"
volumes:
- postgres_kiss_shot_acerola_data:/var/lib/postgresql/data
volumes:
postgres_kiss_shot_acerola_data:

@ -1,11 +1,14 @@
package org.camelia.studio.kiss.shot.acerola;
import org.camelia.studio.kiss.shot.acerola.listeners.ReadyListener;
import org.camelia.studio.kiss.shot.acerola.db.HibernateConfig;
import org.camelia.studio.kiss.shot.acerola.listeners.bot.ReadyListener;
import org.camelia.studio.kiss.shot.acerola.managers.ListenerManager;
import org.camelia.studio.kiss.shot.acerola.utils.Configuration;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.MemberCachePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -20,13 +23,14 @@ public class KissShotAcerola {
jda = JDABuilder.createDefault(Configuration.getInstance().getDotenv().get("BOT_TOKEN"))
.addEventListeners(new ReadyListener())
.enableIntents(GatewayIntent.getIntents(GatewayIntent.ALL_INTENTS))
.setMemberCachePolicy(MemberCachePolicy.ALL)
.build()
.awaitReady()
;
.awaitReady();
new ListenerManager().registerListeners(jda);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
HibernateConfig.shutdown();
jda.shutdown();
}));
} catch (Exception e) {

@ -8,9 +8,10 @@ import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PlayerManager {
@ -39,7 +40,7 @@ public class PlayerManager {
});
}
public void loadAndPlay(TextChannel channel, String url) {
public void loadAndPlay(GuildMessageChannel channel, String url) {
final GuildMusicManager musicManager = getMusicManager(channel.getGuild());
audioPlayerManager.loadItemOrdered(musicManager, url, new AudioLoadResultHandler() {
@ -51,9 +52,14 @@ public class PlayerManager {
@Override
public void playlistLoaded(AudioPlaylist playlist) {
final AudioTrack track = playlist.getTracks().get(0);
musicManager.scheduler.queue(track);
channel.sendMessage("Ajout à la file d'attente: `" + track.getInfo().title + "`").queue();
List<AudioTrack> tracks = playlist.getTracks();
channel.sendMessage(
"Ajout à la file d'attente: `" + playlist.getName() + "` - " + tracks.size() + " musiques.")
.queue();
for (AudioTrack track : tracks)
musicManager.scheduler.queue(track);
}
@Override

@ -1,5 +1,8 @@
package org.camelia.studio.kiss.shot.acerola.audio;
import java.util.LinkedList;
import java.util.Queue;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
@ -7,19 +10,78 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
public class TrackScheduler extends AudioEventAdapter {
private final AudioPlayer player;
private final Queue<AudioTrack> queue;
private boolean loop = false;
private boolean repeat = false;
public TrackScheduler(AudioPlayer player) {
this.player = player;
this.queue = new LinkedList<>();
}
public void queue(AudioTrack track) {
player.startTrack(track, false);
if (!player.startTrack(track, true)) {
queue.offer(track);
}
}
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
if (endReason.mayStartNext) {
// Ici vous pouvez gérer la lecture de la prochaine piste si vous implémentez une file d'attente
nextTrack();
}
}
public void nextTrack() {
AudioTrack track = queue.poll();
if (loop) {
queue.offer(track.makeClone());
} else if (repeat) {
// ON ajoute la track au début de la queue
LinkedList<AudioTrack> list = new LinkedList<>(queue);
queue.clear();
queue.offer(track.makeClone());
while (!list.isEmpty()) {
queue.offer(list.poll());
}
}
player.startTrack(track, false);
}
public void nextTrack(int nextTrack) {
if (nextTrack < 1) {
return;
}
for (int i = 0; i < nextTrack - 1; i++) {
queue.poll();
}
player.startTrack(queue.poll(), false);
}
public Queue<AudioTrack> getQueue() {
return queue;
}
public void clearQueue() {
queue.clear();
}
public void shuffle() {
LinkedList<AudioTrack> list = new LinkedList<>(queue);
queue.clear();
while (!list.isEmpty()) {
int index = (int) (Math.random() * list.size());
queue.offer(list.remove(index));
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
public void setRepeat(boolean repeat) {
this.repeat = repeat;
}
}

@ -0,0 +1,67 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.managers.AudioManager;
import java.awt.Color;
public class NowPlayingCommand implements ISlashCommand {
@Override
public String getName() {
return "nowplaying";
}
@Override
public String getDescription() {
return "Permet de voir la musique en cours de lecture";
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
AudioTrack playingTrack = musicManager.audioPlayer.getPlayingTrack();
if (playingTrack == null) {
event.getHook().editOriginal("La file d'attente est vide.").queue();
return;
}
EmbedBuilder embed = new EmbedBuilder();
embed.setTitle("En cours de lecture");
embed.setColor(Color.ORANGE);
embed.addField("Titre", playingTrack.getInfo().title, false);
embed.addField("Durée", formatTime(playingTrack.getDuration()), false);
embed.addField("Auteur", playingTrack.getInfo().author, false);
embed.addField("Lien", playingTrack.getInfo().uri, false);
embed.setImage(playingTrack.getInfo().artworkUrl);
event.getHook().editOriginalEmbeds(embed.build()).queue();
}
private String formatTime(long timeInMillis) {
long hours = timeInMillis / 3600000;
long minutes = (timeInMillis % 3600000) / 60000;
long seconds = (timeInMillis % 60000) / 1000;
if (hours > 0) {
return String.format("%d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format("%d:%02d", minutes, seconds);
}
}
}

@ -0,0 +1,60 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.managers.AudioManager;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
public class PauseCommand implements ISlashCommand {
@Override
public String getName() {
return "pause";
}
@Override
public String getDescription() {
return "Permet de mettre en pause la musique en cours de lecture";
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
if (!voiceState.inAudioChannel()) {
event.getHook().editOriginal("Vous devez être connecté à un salon vocal pour utiliser cette commande !")
.queue();
return;
}
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (member.getVoiceState().getChannel() != audioManager.getConnectedChannel()) {
event.getHook().editOriginal("Vous devez être dans le même salon vocal que moi pour utiliser cette commande !")
.queue();
return;
}
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
boolean isPaused = musicManager.audioPlayer.isPaused();
musicManager.audioPlayer.setPaused(!isPaused);
if (isPaused) {
event.getHook().editOriginal("La musique a été reprise !").queue();
return;
}
event.getHook().editOriginal("La musique est en pause !").queue();
}
}

@ -0,0 +1,79 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import net.dv8tion.jda.api.audio.hooks.ConnectionListener;
import net.dv8tion.jda.api.audio.hooks.ConnectionStatus;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
public class PlayCommand implements ISlashCommand {
@Override
public String getName() {
return "play";
}
@Override
public String getDescription() {
return "Permet de lancer une musique en .mp3 dans un salon vocal";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.STRING, "url", "URL de la musique à jouer", true));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
String url = event.getOption("url").getAsString();
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
if (!voiceState.inAudioChannel()) {
event.getHook().editOriginal("Vous devez être connecté à un salon vocal pour utiliser cette commande !")
.queue();
return;
}
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
audioManager.openAudioConnection(voiceState.getChannel());
PlayerManager.getInstance().getMusicManager(event.getGuild()).audioPlayer.setVolume(25);
} else if (member.getVoiceState().getChannel() != audioManager.getConnectedChannel()) {
event.getHook().editOriginal("Vous devez être dans le même salon vocal que moi pour utiliser cette commande !")
.queue();
return;
}
audioManager.setConnectionListener(new ConnectionListener() {
@Override
public void onStatusChange(ConnectionStatus status) {
if (status == ConnectionStatus.CONNECTED) {
if (voiceState.getChannel().getType() == ChannelType.STAGE) {
voiceState.getChannel().asStageChannel().requestToSpeak().queue(speakSuccess -> {
audioManager.setConnectionListener(null);
}, error -> {
audioManager.setConnectionListener(null);
});
} else {
audioManager.setConnectionListener(null);
}
}
}
});
PlayerManager.getInstance().loadAndPlay(event.getChannel().asGuildMessageChannel(), url);
event.getHook().editOriginal("Chargement du fichier audio en cours...").queue();
}
}

@ -0,0 +1,126 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
import java.awt.Color;
public class QueueCommand implements ISlashCommand {
final int TRACKS_PER_PAGE = 10;
@Override
public String getName() {
return "queue";
}
@Override
public String getDescription() {
return "Permet de voir la file d'attente";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.INTEGER, "page", "Numéro de la page à visionner").setRequired(false));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
OptionMapping option = event.getOption("page");
int page = 1;
if (option != null) {
page = (int) option.getAsInt();
}
if (page < 1) {
event.getHook().editOriginal("La page doit être supérieure à 0.").queue();
return;
}
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
// On passe aux musiques suivantes
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
Queue<AudioTrack> queue = musicManager.scheduler.getQueue();
if (queue.isEmpty()) {
event.getHook().editOriginal("La file d'attente est vide.").queue();
return;
}
EmbedBuilder embed = new EmbedBuilder();
embed.setTitle("🎵 File d'attente");
embed.setColor(Color.BLUE);
// Calculer le nombre total de pages
int totalPages = (int) Math.ceil((double) queue.size() / TRACKS_PER_PAGE);
if (totalPages == 0)
totalPages = 1;
// Vérifier que la page demandée est valide
if (page < 1 || page > totalPages) {
event.getHook().editOriginal("❌ Page invalide ! (1-" + totalPages + ")").queue();
return;
}
// Afficher la file d'attente pour la page demandée
if (queue.isEmpty()) {
embed.setDescription("Aucune musique dans la file d'attente");
} else {
List<AudioTrack> trackList = new ArrayList<>(queue);
int startIndex = (page - 1) * TRACKS_PER_PAGE;
int endIndex = Math.min(startIndex + TRACKS_PER_PAGE, trackList.size());
StringBuilder queueList = new StringBuilder();
for (int i = startIndex; i < endIndex; i++) {
AudioTrack track = trackList.get(i);
queueList.append(i + 1)
.append(". `")
.append(track.getInfo().title)
.append("` [")
.append(formatTime(track.getDuration()))
.append("]\n");
}
embed.setDescription(queueList.toString());
}
long totalDuration = queue.stream().mapToLong(AudioTrack::getDuration).sum();
embed.setFooter(String.format("Page %d/%d • %d musiques • Durée totale: %s",
page, totalPages, queue.size(), formatTime(totalDuration)));
event.getHook().editOriginalEmbeds(embed.build()).queue();
}
private String formatTime(long timeInMillis) {
long hours = timeInMillis / 3600000;
long minutes = (timeInMillis % 3600000) / 60000;
long seconds = (timeInMillis % 60000) / 1000;
if (hours > 0) {
return String.format("%d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format("%d:%02d", minutes, seconds);
}
}
}

@ -0,0 +1,87 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
public class RepeatCommand implements ISlashCommand {
@Override
public String getName() {
return "repeat";
}
@Override
public String getDescription() {
return "Permet de répéter la musique en cours";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.STRING, "mode", "Le mode de répétition").addChoice("Toute la queue", "all")
.addChoice("La musique actuelle", "one").addChoice("Désactiver la répétition", "off")
.setRequired(true));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
if (!voiceState.inAudioChannel()) {
event.getHook().editOriginal("Vous devez être connecté à un salon vocal pour utiliser cette commande !")
.queue();
return;
}
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (member.getVoiceState().getChannel() != audioManager.getConnectedChannel()) {
event.getHook()
.editOriginal("Vous devez être dans le même salon vocal que moi pour utiliser cette commande !")
.queue();
return;
}
String mode = event.getOption("mode").getAsString();
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
switch (mode) {
case "all":
musicManager.scheduler.setLoop(true);
musicManager.scheduler.setRepeat(false);
event.getHook().editOriginal("Toute la queue sera répétée !").queue();
break;
case "one":
musicManager.scheduler.setLoop(false);
musicManager.scheduler.setRepeat(true);
event.getHook().editOriginal("La musique actuelle sera répétée !").queue();
break;
case "off":
musicManager.scheduler.setLoop(false);
musicManager.scheduler.setRepeat(false);
event.getHook().editOriginal("La répétition a été désactivée !").queue();
break;
}
}
}

@ -0,0 +1,53 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.managers.AudioManager;
public class ShuffleCommand implements ISlashCommand {
@Override
public String getName() {
return "shuffle";
}
@Override
public String getDescription() {
return "Permet de mélanger la file d'attente";
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
// Vérifier si l'utilisateur est dans un canal vocal
GuildVoiceState voiceState = event.getMember().getVoiceState();
if (!voiceState.inAudioChannel()) {
event.getHook().editOriginal("Vous devez être dans un canal vocal pour utiliser cette commande !").queue();
return;
}
// Vérifier si le bot est dans le même canal vocal
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (voiceState.getChannel() != audioManager.getConnectedChannel()) {
event.getHook().editOriginal("Vous devez être dans le même canal vocal que moi !").queue();
return;
}
// On passe aux musiques suivantes
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
musicManager.scheduler.shuffle();
event.getHook().editOriginal("La file d'attente a été mélangée !").queue();
}
}

@ -0,0 +1,70 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
public class SkipCommand implements ISlashCommand {
@Override
public String getName() {
return "skip";
}
@Override
public String getDescription() {
return "Permet de passer à la musique suivante";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.INTEGER, "tracknumber", "Nombre de musique à passer").setRequired(false));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
OptionMapping option = event.getOption("tracknumber");
int skipAmount = 1;
if (option != null) {
skipAmount = (int) option.getAsInt();
}
// Vérifier si l'utilisateur est dans un canal vocal
GuildVoiceState voiceState = event.getMember().getVoiceState();
if (!voiceState.inAudioChannel()) {
event.getHook().editOriginal("Vous devez être dans un canal vocal pour utiliser cette commande !").queue();
return;
}
// Vérifier si le bot est dans le même canal vocal
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (voiceState.getChannel() != audioManager.getConnectedChannel()) {
event.getHook().editOriginal("Vous devez être dans le même canal vocal que moi !").queue();
return;
}
// On passe aux musiques suivantes
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
musicManager.scheduler.nextTrack(skipAmount);
event.getHook().editOriginal("Passage de %d musiques.".formatted(skipAmount)).queue();
}
}

@ -1,4 +1,4 @@
package org.camelia.studio.kiss.shot.acerola.commands.utils;
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
@ -8,11 +8,11 @@ import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.managers.AudioManager;
public class StopAudioCommand implements ISlashCommand{
public class StopCommand implements ISlashCommand {
@Override
public String getName() {
return "stopaudio";
return "stop";
}
@Override
@ -35,7 +35,7 @@ public class StopAudioCommand implements ISlashCommand{
event.reply("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (voiceState.getChannel() != audioManager.getConnectedChannel()) {
event.reply("Vous devez être dans le même canal vocal que moi !").queue();
return;
@ -44,10 +44,10 @@ public class StopAudioCommand implements ISlashCommand{
// Arrêter la musique
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
musicManager.audioPlayer.stopTrack();
// Déconnecter le bot
audioManager.closeAudioConnection();
event.reply("Musique arrêtée et déconnexion du canal vocal.").queue();
}

@ -0,0 +1,63 @@
package org.camelia.studio.kiss.shot.acerola.commands.audio;
import org.camelia.studio.kiss.shot.acerola.audio.GuildMusicManager;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
import java.util.List;
public class VolumeCommand implements ISlashCommand {
@Override
public String getName() {
return "volume";
}
@Override
public String getDescription() {
return "Permet de changer le volume du bot";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.INTEGER, "volume", "Le volume souhaité").setRequired(true).setMinValue(0).setMaxValue(100)
);
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
GuildVoiceState voiceState = event.getMember().getVoiceState();
if (!voiceState.inAudioChannel()) {
event.reply("Vous devez être dans un canal vocal pour utiliser cette commande !").queue();
return;
}
AudioManager audioManager = event.getGuild().getAudioManager();
if (!audioManager.isConnected()) {
event.getHook().editOriginal("Je ne suis pas connecté à un canal vocal !").queue();
return;
}
if (voiceState.getChannel() != audioManager.getConnectedChannel()) {
event.getHook().editOriginal("Vous devez être dans le même canal vocal que moi !").queue();
return;
}
GuildMusicManager musicManager = PlayerManager.getInstance().getMusicManager(event.getGuild());
int volume = Integer.parseInt(event.getOption("volume").getAsString());
musicManager.audioPlayer.setVolume(volume);
event.getHook().editOriginal("Le volume a été changé à " + volume + "%").queue();
}
}

@ -0,0 +1,126 @@
package org.camelia.studio.kiss.shot.acerola.commands.moderation;
import java.io.File;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import org.camelia.studio.kiss.shot.acerola.models.Averto;
import org.camelia.studio.kiss.shot.acerola.models.User;
import org.camelia.studio.kiss.shot.acerola.repositories.AvertoRepository;
import org.camelia.studio.kiss.shot.acerola.services.UserService;
import org.camelia.studio.kiss.shot.acerola.utils.Configuration;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Message.Attachment;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.utils.FileUpload;
public class AvertoCommand implements ISlashCommand {
@Override
public String getName() {
return "averto";
}
@Override
public String getDescription() {
return "Permet d'avertir un utilisateur";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.USER, "utilisateur", "L'utilisateur à avertir", true),
new OptionData(OptionType.STRING, "raison", "La raison de l'avertissement", false),
new OptionData(OptionType.ATTACHMENT, "file", "Une preuve de l'avertissement", false));
}
@Override
public DefaultMemberPermissions defaultPermissions() {
return DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS);
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().setEphemeral(true).queue();
try {
Member moderator = event.getMember();
Member member = event.getOption("utilisateur").getAsMember();
OptionMapping raisonOptionMapping = event.getOption("raison");
String reason = raisonOptionMapping == null ? "Aucune raison spécifiée" : raisonOptionMapping.getAsString();
OptionMapping fileOptionMapping = event.getOption("file");
Attachment file = null;
String fileUrl = null;
TextChannel logChannel = event.getGuild()
.getTextChannelById(Configuration.getInstance().getDotenv().get("LOG_CHANNEL_ID"));
if (fileOptionMapping != null) {
file = fileOptionMapping.getAsAttachment();
}
if (logChannel != null) {
File fileTemp = null;
if (file != null) {
fileTemp = File.createTempFile("proof_" + member.getId() + "_", "." + file.getFileExtension());
fileTemp = file.getProxy().downloadToFile(fileTemp).get();
}
Message message = this.sendLogMessage(logChannel, member, fileTemp, reason);
if (fileTemp != null) {
fileUrl = message.getAttachments().get(0).getUrl();
fileTemp.delete();
}
}
User memberUser = UserService.getInstance().getOrCreateUser(member.getId());
User moderatorUser = UserService.getInstance().getOrCreateUser(moderator.getId());
Averto averto = new Averto(memberUser, moderatorUser);
averto.setReason(reason);
averto.setFile(fileUrl);
AvertoRepository.getInstance().save(averto);
// On tente d'envoyer un message privé à l'utilisateur averti
member.getUser().openPrivateChannel().queue(privateChannel -> {
privateChannel
.sendMessage("Bonjour, Vous avez été averti sur %s pour la raison suivante : %s".formatted(
event.getGuild().getName(), reason != null ? reason : "Aucune raison spécifiée"))
.queue();
});
event.getHook().editOriginal("L'utilisateur %s a bien été averti !".formatted(member.getAsMention()))
.queue();
} catch (Exception e) {
event.getHook().editOriginal("Une erreur est survenue lors de l'avertissement, " + e.getMessage()).queue();
}
}
private Message sendLogMessage(TextChannel logChannel, Member member, File fileTemp, String reason) {
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setTitle("Avertissement - Règlement enfreint");
embedBuilder.setDescription("Un utilisateur a été averti pour non respect du règlement");
embedBuilder.addField("Utilisateur", member.getAsMention(), false);
embedBuilder.addField("Raison", reason != null ? reason : "Aucune raison spécifié", false);
Message msg = logChannel.sendMessageEmbeds(embedBuilder.build()).complete();
if (fileTemp != null) {
msg = logChannel
.sendFiles(FileUpload.fromData(fileTemp)).complete();
}
return msg;
}
}

@ -0,0 +1,99 @@
package org.camelia.studio.kiss.shot.acerola.commands.moderation;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
import org.camelia.studio.kiss.shot.acerola.models.Averto;
import org.camelia.studio.kiss.shot.acerola.models.User;
import org.camelia.studio.kiss.shot.acerola.services.AvertoService;
import org.camelia.studio.kiss.shot.acerola.services.UserService;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
public class AvertoListCommand implements ISlashCommand {
@Override
public String getName() {
return "avertolist";
}
@Override
public String getDescription() {
return "Liste les avertissements d'un utilisateur";
}
@Override
public DefaultMemberPermissions defaultPermissions() {
return DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS);
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(
OptionType.USER,
"utilisateur",
"L'utilisateur dont vous voulez voir les avertissements",
false));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().setEphemeral(true).queue();
OptionMapping option = event.getOption("utilisateur");
Member member = null;
List<Averto> avertos = null;
User user = null;
if (option != null) {
member = option.getAsMember();
user = UserService.getInstance().getOrCreateUser(member.getId());
avertos = user.getAvertos();
} else {
avertos = AvertoService.getInstance().getLatestAvertos(10);
}
/*
* 2 possibilités :
* - Aucun utilisateur : On affiche les 10 derniers avertissements du serveur
* - Un utilisateur : On affiche les avertissements de cet utilisateur
*/
EmbedBuilder embedBuilder = new EmbedBuilder()
.setTitle("Avertissements de " + (member == null ? "tous les utilisateurs" : member.getEffectiveName()))
.setColor(0xFF0000);
int count = 0;
for (Averto averto : avertos) {
count++;
if (count > 10) {
break;
}
// On récupère le membre Discord de l'utilisateur
Member discordUser = event.getGuild().getMemberById(averto.getUser().getDiscordId());
Member moderator = event.getGuild().getMemberById(averto.getModerator().getDiscordId());
embedBuilder.addField(
"Avertissement #" + averto.getId(),
(discordUser != null ? "Utilisateur : " + discordUser.getAsMention() + "\n" : "") +
"Raison : " + averto.getReason() + "\n" +
(moderator != null ? "Modérateur : " + moderator.getAsMention() : "") + "\n" +
"Date : "
+ averto.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")) +
"\n" +
"Preuve : " + (averto.getFile() != null ? averto.getFile() : "Aucune"),
false);
}
event.getHook().editOriginalEmbeds(embedBuilder.build()).queue();
}
}

@ -5,7 +5,6 @@ import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;

@ -1,56 +0,0 @@
package org.camelia.studio.kiss.shot.acerola.commands.utils;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.managers.AudioManager;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import org.camelia.studio.kiss.shot.acerola.interfaces.ISlashCommand;
public class PlayAudioCommand implements ISlashCommand {
@Override
public String getName() {
return "playaudio";
}
@Override
public String getDescription() {
return "Permet de lancer une musique en .mp3 dans un salon vocal";
}
@Override
public List<OptionData> getOptions() {
return List.of(
new OptionData(OptionType.STRING, "url", "URL de la musique à jouer", true));
}
@Override
public void execute(SlashCommandInteractionEvent event) {
event.deferReply().queue();
String url = event.getOption("url").getAsString();
Member member = event.getMember();
GuildVoiceState voiceState = member.getVoiceState();
if (!member.getVoiceState().inAudioChannel()) {
event.getHook().editOriginal("Vous devez être connecté à un salon vocal pour utiliser cette commande !")
.queue();
return;
}
VoiceChannel channel = voiceState.getChannel().asVoiceChannel();
AudioManager audioManager = event.getGuild().getAudioManager();
audioManager.openAudioConnection(channel);
PlayerManager.getInstance().getMusicManager(event.getGuild()).audioPlayer.setVolume(25);
PlayerManager.getInstance().loadAndPlay(event.getChannel().asTextChannel(), url);
event.getHook().editOriginal("Chargement du fichier audio en cours...").queue();
}
}

@ -0,0 +1,83 @@
package org.camelia.studio.kiss.shot.acerola.db;
import io.github.cdimascio.dotenv.Dotenv;
import org.camelia.studio.kiss.shot.acerola.interfaces.IEntity;
import org.camelia.studio.kiss.shot.acerola.utils.ReflectionUtils;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.service.ServiceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Properties;
public class HibernateConfig {
private static final Logger logger = LoggerFactory.getLogger(HibernateConfig.class);
private static SessionFactory sessionFactory;
public static SessionFactory getSessionFactory() {
if (sessionFactory == null) {
try {
logger.info("Initializing Hibernate SessionFactory");
Dotenv dotenv = org.camelia.studio.kiss.shot.acerola.utils.Configuration.getInstance().getDotenv();
Properties props = new Properties();
// Configuration Hibernate
props.put(Environment.HBM2DDL_AUTO, "update"); // On utilise validate au lieu de update
props.put(Environment.GLOBALLY_QUOTED_IDENTIFIERS, "true");
// Configuration HikariCP
props.put("hibernate.connection.provider_class",
"org.hibernate.hikaricp.internal.HikariCPConnectionProvider");
props.put("hibernate.hikari.minimumIdle", "5");
props.put("hibernate.hikari.maximumPoolSize", "10");
props.put("hibernate.hikari.idleTimeout", "300000");
props.put("hibernate.hikari.dataSourceClassName",
"org.postgresql.ds.PGSimpleDataSource");
props.put("hibernate.hikari.dataSource.url", dotenv.get("DB_URL"));
props.put("hibernate.hikari.dataSource.user", dotenv.get("DB_USER"));
props.put("hibernate.hikari.dataSource.password", dotenv.get("DB_PASSWORD"));
Configuration configuration = new Configuration();
configuration.setProperties(props);
List<IEntity> entities = ReflectionUtils.loadClasses(
"org.camelia.studio.kiss.shot.acerola.models",
IEntity.class);
for (IEntity entity : entities) {
configuration.addAnnotatedClass(entity.getClass());
}
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties())
.build();
sessionFactory = configuration.buildSessionFactory(serviceRegistry);
logger.info("Hibernate SessionFactory initialized successfully");
} catch (Exception e) {
logger.error("Failed to initialize Hibernate SessionFactory", e);
throw new RuntimeException("Failed to initialize Hibernate SessionFactory", e);
}
}
return sessionFactory;
}
public static void shutdown() {
logger.info("Shutting down database connections");
if (sessionFactory != null && !sessionFactory.isClosed()) {
try {
sessionFactory.close();
logger.info("SessionFactory closed successfully");
} catch (Exception e) {
logger.error("Error closing SessionFactory", e);
}
}
}
}

@ -0,0 +1,4 @@
package org.camelia.studio.kiss.shot.acerola.interfaces;
public interface IEntity {
}

@ -1,16 +1,23 @@
package org.camelia.studio.kiss.shot.acerola.interfaces;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import java.util.List;
public interface ISlashCommand {
String getName();
String getDescription();
void execute(SlashCommandInteractionEvent event);
default List<OptionData> getOptions() {
return List.of();
}
}
default DefaultMemberPermissions defaultPermissions() {
return DefaultMemberPermissions.ENABLED;
}
}

@ -1,4 +1,4 @@
package org.camelia.studio.kiss.shot.acerola.listeners;
package org.camelia.studio.kiss.shot.acerola.listeners.bot;
import org.camelia.studio.kiss.shot.acerola.utils.Configuration;
import net.dv8tion.jda.api.JDA;

@ -1,4 +1,4 @@
package org.camelia.studio.kiss.shot.acerola.listeners;
package org.camelia.studio.kiss.shot.acerola.listeners.global;
import org.camelia.studio.kiss.shot.acerola.utils.Configuration;
import net.dv8tion.jda.api.entities.Member;
@ -6,9 +6,6 @@ import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.awt.*;
public class GuildMemberJoinListener extends ListenerAdapter {
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent event) {

@ -1,4 +1,4 @@
package org.camelia.studio.kiss.shot.acerola.listeners;
package org.camelia.studio.kiss.shot.acerola.listeners.global;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;

@ -1,9 +1,9 @@
package org.camelia.studio.kiss.shot.acerola.listeners;
package org.camelia.studio.kiss.shot.acerola.listeners.global;
import org.camelia.studio.kiss.shot.acerola.audio.PlayerManager;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.managers.AudioManager;
@ -19,7 +19,7 @@ public class VoiceLeaveListener extends ListenerAdapter {
return;
}
VoiceChannel botChannel = audioManager.getConnectedChannel().asVoiceChannel();
AudioChannel botChannel = audioManager.getConnectedChannel();
// Compte le nombre de membres dans le canal (excluant les bots)
long realMembersCount = botChannel.getMembers().stream()

@ -2,13 +2,11 @@ package org.camelia.studio.kiss.shot.acerola.managers;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.camelia.studio.kiss.shot.acerola.listeners.GuildMemberJoinListener;
import org.camelia.studio.kiss.shot.acerola.listeners.SlashCommandListener;
import org.camelia.studio.kiss.shot.acerola.listeners.VoiceLeaveListener;
import org.camelia.studio.kiss.shot.acerola.utils.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class ListenerManager {
@ -16,11 +14,9 @@ public class ListenerManager {
private final Logger logger = LoggerFactory.getLogger(ListenerManager.class.getName());
public ListenerManager() {
listener = new ArrayList<>();
addListener(new SlashCommandListener());
addListener(new GuildMemberJoinListener());
addListener(new VoiceLeaveListener());
listener = ReflectionUtils.loadClasses(
"org.camelia.studio.kiss.shot.acerola.listeners.global",
ListenerAdapter.class);
}
public void registerListeners(JDA jda) {
@ -30,8 +26,4 @@ public class ListenerManager {
logger.info("Listener {} enregistré !", listenerAdapter.getClass().getSimpleName());
}
}
private void addListener(ListenerAdapter listenerAdapter) {
this.listener.add(listenerAdapter);
}
}

@ -0,0 +1,81 @@
package org.camelia.studio.kiss.shot.acerola.models;
import jakarta.persistence.*;
import org.camelia.studio.kiss.shot.acerola.interfaces.IEntity;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "avertos")
public class Averto implements IEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
private User user;
@ManyToOne(fetch = FetchType.EAGER)
private User moderator;
@Column(name = "reason", nullable = true, unique = false)
private String reason;
@Column(name = "file", nullable = true, unique = false)
private String file;
@CreationTimestamp
@Column(name = "createdAt")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updatedAt")
private LocalDateTime updatedAt;
public Averto() {
}
public Averto(User user, User moderator) {
this.user = user;
this.moderator = moderator;
}
public Long getId() {
return id;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public String getReason() {
return reason;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public void setReason(String reason) {
this.reason = reason;
}
public User getModerator() {
return moderator;
}
public User getUser() {
return user;
}
}

@ -0,0 +1,70 @@
package org.camelia.studio.kiss.shot.acerola.models;
import jakarta.persistence.*;
import org.camelia.studio.kiss.shot.acerola.interfaces.IEntity;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "users")
public class User implements IEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Averto> avertos;
@OneToMany(mappedBy = "moderator", fetch = FetchType.EAGER)
private List<Averto> moderatedAvertos;
@Column(name = "discordId", nullable = false, unique = true)
private String discordId;
@CreationTimestamp
@Column(name = "createdAt")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updatedAt")
private LocalDateTime updatedAt;
public User() {
}
public User(String discordId) {
this.discordId = discordId;
}
public Long getId() {
return id;
}
public String getDiscordId() {
return discordId;
}
public void setDiscordId(String discordId) {
this.discordId = discordId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public List<Averto> getAvertos() {
return avertos;
}
public List<Averto> getModeratedAvertos() {
return moderatedAvertos;
}
}

@ -0,0 +1,53 @@
package org.camelia.studio.kiss.shot.acerola.repositories;
import org.camelia.studio.kiss.shot.acerola.db.HibernateConfig;
import org.camelia.studio.kiss.shot.acerola.models.Averto;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Order;
import org.hibernate.query.SortDirection;
import java.util.List;
public class AvertoRepository {
private final SessionFactory sessionFactory;
private static AvertoRepository instance;
public static AvertoRepository getInstance() {
if (instance == null) {
instance = new AvertoRepository();
}
return instance;
}
public AvertoRepository() {
this.sessionFactory = HibernateConfig.getSessionFactory();
}
public List<Averto> findAll() {
try (Session session = sessionFactory.openSession()) {
return session.createQuery("FROM User", Averto.class)
.setOrder(Order.by(Averto.class, "createdAt", SortDirection.DESCENDING))
.list();
}
}
public List<Averto> findCount(int count) {
try (Session session = sessionFactory.openSession()) {
return session.createQuery("FROM Averto", Averto.class)
.setOrder(Order.by(Averto.class, "createdAt", SortDirection.DESCENDING))
.setMaxResults(count)
.list();
}
}
public Averto save(Averto averto) {
try (Session session = sessionFactory.openSession()) {
session.beginTransaction();
session.persist(averto);
session.getTransaction().commit();
return averto;
}
}
}

@ -0,0 +1,56 @@
package org.camelia.studio.kiss.shot.acerola.repositories;
import org.camelia.studio.kiss.shot.acerola.db.HibernateConfig;
import org.camelia.studio.kiss.shot.acerola.models.User;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import java.util.List;
public class UserRepository {
private final SessionFactory sessionFactory;
private static UserRepository instance;
public static UserRepository getInstance() {
if (instance == null) {
instance = new UserRepository();
}
return instance;
}
public UserRepository() {
this.sessionFactory = HibernateConfig.getSessionFactory();
}
public List<User> findAll() {
try (Session session = sessionFactory.openSession()) {
return session.createQuery("FROM User", User.class).list();
}
}
public User findByDiscordId(String discordId) {
try (Session session = sessionFactory.openSession()) {
return session.createQuery("FROM User WHERE discordId = :discordId", User.class)
.setParameter("discordId", discordId)
.uniqueResult();
}
}
public User save(User user) {
try (Session session = sessionFactory.openSession()) {
session.beginTransaction();
session.persist(user);
session.getTransaction().commit();
return user;
}
}
public void update(User user) {
try (Session session = sessionFactory.openSession()) {
session.beginTransaction();
session.merge(user);
session.getTransaction().commit();
}
}
}

@ -0,0 +1,23 @@
package org.camelia.studio.kiss.shot.acerola.services;
import java.util.List;
import org.camelia.studio.kiss.shot.acerola.models.Averto;
import org.camelia.studio.kiss.shot.acerola.repositories.AvertoRepository;
public class AvertoService {
private static AvertoService instance;
public static AvertoService getInstance() {
if (instance == null) {
instance = new AvertoService();
}
return instance;
}
public List<Averto> getLatestAvertos(int amount) {
return AvertoRepository.getInstance().findCount(amount);
}
}

@ -0,0 +1,37 @@
package org.camelia.studio.kiss.shot.acerola.services;
import org.camelia.studio.kiss.shot.acerola.models.*;
import org.camelia.studio.kiss.shot.acerola.repositories.UserRepository;
import java.util.List;
public class UserService {
private static UserService instance;
public static UserService getInstance() {
if (instance == null) {
instance = new UserService();
}
return instance;
}
public User getOrCreateUser(String discordId) {
User user = UserRepository.getInstance().findByDiscordId(discordId);
if (user == null) {
user = new User(discordId);
UserRepository.getInstance().save(user);
}
return user;
}
public List<User> getAllUsers() {
return UserRepository.getInstance().findAll();
}
public void updateUser(User user) {
UserRepository.getInstance().update(user);
}
}