Ajout du système d'avertissement

This commit is contained in:
Melaine Gérard 2025-01-04 22:08:38 +01:00
parent 2f020dc5a7
commit 1cce7f01b7
15 changed files with 668 additions and 4 deletions

View File

@ -2,3 +2,7 @@ BOT_TOKEN=
GUILD_ID=
DEFAULT_ROLE_ID=
ROLE_ID=
LOG_CHANNEL_ID=
DB_URL=jdbc:postgresql://localhost:5434/kiss_shot_acerola
DB_USER=postgres
DB_PASSWORD=kiss_shot_acerola

View File

@ -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
View 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:

View File

@ -1,11 +1,14 @@
package org.camelia.studio.kiss.shot.acerola;
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) {

View File

@ -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.MESSAGE_MANAGE);
}
@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;
}
}

View File

@ -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.MESSAGE_MANAGE);
}
@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();
}
}

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

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

View File

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