diff --git a/src/main/java/creator/loki/feature/moderation/ModerationModule.java b/src/main/java/creator/loki/feature/moderation/ModerationModule.java index a73d446..0c0f3f9 100644 --- a/src/main/java/creator/loki/feature/moderation/ModerationModule.java +++ b/src/main/java/creator/loki/feature/moderation/ModerationModule.java @@ -7,6 +7,9 @@ import creator.loki.database.Database; import creator.loki.feature.config.repository.GuildConfigRepository; import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ModerationCaseRepository; +import creator.loki.feature.moderation.repository.SqlActiveMuteRepository; +import creator.loki.feature.moderation.repository.SqlModerationCaseRepository; +import creator.loki.feature.moderation.repository.SqlWarningRepository; import creator.loki.feature.moderation.repository.WarningRepository; public class ModerationModule extends AbstractModule { @@ -17,19 +20,19 @@ public class ModerationModule extends AbstractModule { @Provides @Singleton WarningRepository provideWarningRepository(Database database) { - return new WarningRepository(database); + return new SqlWarningRepository(database); } @Provides @Singleton ModerationCaseRepository provideModerationCaseRepository(Database database) { - return new ModerationCaseRepository(database); + return new SqlModerationCaseRepository(database); } @Provides @Singleton ActiveMuteRepository provideActiveMuteRepository(Database database) { - return new ActiveMuteRepository(database); + return new SqlActiveMuteRepository(database); } @Provides diff --git a/src/main/java/creator/loki/feature/moderation/repository/ActiveMuteRepository.java b/src/main/java/creator/loki/feature/moderation/repository/ActiveMuteRepository.java index 94932fc..be9d398 100644 --- a/src/main/java/creator/loki/feature/moderation/repository/ActiveMuteRepository.java +++ b/src/main/java/creator/loki/feature/moderation/repository/ActiveMuteRepository.java @@ -1,104 +1,16 @@ package creator.loki.feature.moderation.repository; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import creator.loki.database.Database; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +public interface ActiveMuteRepository { + void upsert(String guildId, String userId, long endsAtMs, long caseId); -/** Активные муты (для снятия роли по таймеру и восстановления после рестарта). */ -public class ActiveMuteRepository { - private static final Logger logger = LoggerFactory.getLogger(ActiveMuteRepository.class); - private final Database database; + void delete(String guildId, String userId); - public ActiveMuteRepository(Database database) { - this.database = database; - } + Optional find(String guildId, String userId); - /** - * @param endsAtMs {@code 0} - бессрочный мут (таймер не ставится) - */ - public void upsert(String guildId, String userId, long endsAtMs, long caseId) { - String sql = - """ - INSERT INTO active_mutes (guild_id, user_id, ends_at_ms, case_id) - VALUES (?, ?, ?, ?) - ON CONFLICT(guild_id, user_id) DO UPDATE SET ends_at_ms = excluded.ends_at_ms, case_id = excluded.case_id - """; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - stmt.setLong(3, endsAtMs); - stmt.setLong(4, caseId); - stmt.executeUpdate(); - } catch (SQLException e) { - logger.error("Ошибка upsert active_mutes guild={} user={}", guildId, userId, e); - } - } + List listTimedAfter(long nowMs); - public void delete(String guildId, String userId) { - String sql = "DELETE FROM active_mutes WHERE guild_id = ? AND user_id = ?"; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - stmt.executeUpdate(); - } catch (SQLException e) { - logger.error("Ошибка delete active_mutes", e); - } - } - - public Optional find(String guildId, String userId) { - String sql = - "SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes WHERE guild_id = ? AND user_id = ?"; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - return Optional.of( - new ActiveMuteEntry( - rs.getString("guild_id"), - rs.getString("user_id"), - rs.getLong("ends_at_ms"), - rs.getLong("case_id"))); - } - } - } catch (SQLException e) { - logger.error("Ошибка find active_mutes", e); - } - return Optional.empty(); - } - - /** Муты с таймером, которые ещё должны истечь после {@code nowMs}. */ - public List listTimedAfter(long nowMs) { - String sql = - """ - SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes - WHERE ends_at_ms > ? AND ends_at_ms > 0 - """; - List list = new ArrayList<>(); - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setLong(1, nowMs); - try (ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - list.add( - new ActiveMuteEntry( - rs.getString("guild_id"), - rs.getString("user_id"), - rs.getLong("ends_at_ms"), - rs.getLong("case_id"))); - } - } - } catch (SQLException e) { - logger.error("Ошибка list active_mutes", e); - } - return list; - } - - public record ActiveMuteEntry(String guildId, String userId, long endsAtMs, long caseId) {} + record ActiveMuteEntry(String guildId, String userId, long endsAtMs, long caseId) {} } diff --git a/src/main/java/creator/loki/feature/moderation/repository/ModerationCaseRepository.java b/src/main/java/creator/loki/feature/moderation/repository/ModerationCaseRepository.java index 8f87805..bd272dd 100644 --- a/src/main/java/creator/loki/feature/moderation/repository/ModerationCaseRepository.java +++ b/src/main/java/creator/loki/feature/moderation/repository/ModerationCaseRepository.java @@ -1,110 +1,20 @@ package creator.loki.feature.moderation.repository; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; import java.util.Optional; -import creator.loki.database.Database; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Кейсы модерации (мут и т.п.) для команды {@code !case}. */ -public class ModerationCaseRepository { - private static final Logger logger = LoggerFactory.getLogger(ModerationCaseRepository.class); - private final Database database; - - public ModerationCaseRepository(Database database) { - this.database = database; - } - - /** - * @param durationSec длительность в секундах или {@code null} (без срока / N/A) - * @param untilMs время окончания мута (epoch ms) или {@code null} - */ - public long insert( +public interface ModerationCaseRepository { + long insert( String guildId, String action, String targetId, String moderatorId, String reason, Long durationSec, - Long untilMs) { - String sql = - """ - INSERT INTO moderation_cases (guild_id, action, target_id, moderator_id, - reason, created_at, duration_sec, until_ms) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """; - long now = System.currentTimeMillis(); - try (Connection conn = database.getConnection(); - PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - stmt.setString(1, guildId); - stmt.setString(2, action); - stmt.setString(3, targetId); - stmt.setString(4, moderatorId); - stmt.setString(5, reason); - stmt.setLong(6, now); - if (durationSec == null) { - stmt.setNull(7, java.sql.Types.INTEGER); - } else { - stmt.setLong(7, durationSec); - } - if (untilMs == null) { - stmt.setNull(8, java.sql.Types.BIGINT); - } else { - stmt.setLong(8, untilMs); - } - stmt.executeUpdate(); - try (ResultSet keys = stmt.getGeneratedKeys()) { - if (keys.next()) { - return keys.getLong(1); - } - } - } catch (SQLException e) { - logger.error("Ошибка записи moderation_cases guild={}", guildId, e); - } - return -1; - } + Long untilMs); - public Optional findById(String guildId, long caseId) { - String sql = - """ - SELECT id, guild_id, action, target_id, moderator_id, reason, created_at, duration_sec, until_ms - FROM moderation_cases WHERE id = ? AND guild_id = ? - """; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setLong(1, caseId); - stmt.setString(2, guildId); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - return Optional.of(map(rs)); - } - } - } catch (SQLException e) { - logger.error("Ошибка чтения case id={}", caseId, e); - } - return Optional.empty(); - } + Optional findById(String guildId, long caseId); - private static CaseEntry map(ResultSet rs) throws SQLException { - Long duration = rs.getObject("duration_sec") == null ? null : rs.getLong("duration_sec"); - Long until = rs.getObject("until_ms") == null ? null : rs.getLong("until_ms"); - return new CaseEntry( - rs.getLong("id"), - rs.getString("guild_id"), - rs.getString("action"), - rs.getString("target_id"), - rs.getString("moderator_id"), - rs.getString("reason"), - rs.getLong("created_at"), - duration, - until); - } - - public record CaseEntry( + record CaseEntry( long id, String guildId, String action, diff --git a/src/main/java/creator/loki/feature/moderation/repository/SqlActiveMuteRepository.java b/src/main/java/creator/loki/feature/moderation/repository/SqlActiveMuteRepository.java new file mode 100644 index 0000000..4abfb04 --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/repository/SqlActiveMuteRepository.java @@ -0,0 +1,101 @@ +package creator.loki.feature.moderation.repository; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import creator.loki.database.Database; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SqlActiveMuteRepository implements ActiveMuteRepository { + private static final Logger logger = LoggerFactory.getLogger(SqlActiveMuteRepository.class); + private final Database database; + + public SqlActiveMuteRepository(Database database) { + this.database = database; + } + + @Override + public void upsert(String guildId, String userId, long endsAtMs, long caseId) { + String sql = + """ + INSERT INTO active_mutes (guild_id, user_id, ends_at_ms, case_id) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET ends_at_ms = excluded.ends_at_ms, case_id = excluded.case_id + """; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + stmt.setLong(3, endsAtMs); + stmt.setLong(4, caseId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.error("Ошибка upsert active_mutes guild={} user={}", guildId, userId, e); + } + } + + @Override + public void delete(String guildId, String userId) { + String sql = "DELETE FROM active_mutes WHERE guild_id = ? AND user_id = ?"; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.error("Ошибка delete active_mutes", e); + } + } + + @Override + public Optional find(String guildId, String userId) { + String sql = + "SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes WHERE guild_id = ? AND user_id = ?"; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of( + new ActiveMuteEntry( + rs.getString("guild_id"), + rs.getString("user_id"), + rs.getLong("ends_at_ms"), + rs.getLong("case_id"))); + } + } + } catch (SQLException e) { + logger.error("Ошибка find active_mutes", e); + } + return Optional.empty(); + } + + @Override + public List listTimedAfter(long nowMs) { + String sql = + """ + SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes + WHERE ends_at_ms > ? AND ends_at_ms > 0 + """; + List list = new ArrayList<>(); + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setLong(1, nowMs); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + list.add( + new ActiveMuteEntry( + rs.getString("guild_id"), + rs.getString("user_id"), + rs.getLong("ends_at_ms"), + rs.getLong("case_id"))); + } + } + } catch (SQLException e) { + logger.error("Ошибка list active_mutes", e); + } + return list; + } +} diff --git a/src/main/java/creator/loki/feature/moderation/repository/SqlModerationCaseRepository.java b/src/main/java/creator/loki/feature/moderation/repository/SqlModerationCaseRepository.java new file mode 100644 index 0000000..5189729 --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/repository/SqlModerationCaseRepository.java @@ -0,0 +1,103 @@ +package creator.loki.feature.moderation.repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Optional; + +import creator.loki.database.Database; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SqlModerationCaseRepository implements ModerationCaseRepository { + private static final Logger logger = LoggerFactory.getLogger(SqlModerationCaseRepository.class); + private final Database database; + + public SqlModerationCaseRepository(Database database) { + this.database = database; + } + + @Override + public long insert( + String guildId, + String action, + String targetId, + String moderatorId, + String reason, + Long durationSec, + Long untilMs) { + String sql = + """ + INSERT INTO moderation_cases (guild_id, action, target_id, moderator_id, + reason, created_at, duration_sec, until_ms) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """; + long now = System.currentTimeMillis(); + try (Connection conn = database.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, guildId); + stmt.setString(2, action); + stmt.setString(3, targetId); + stmt.setString(4, moderatorId); + stmt.setString(5, reason); + stmt.setLong(6, now); + if (durationSec == null) { + stmt.setNull(7, java.sql.Types.INTEGER); + } else { + stmt.setLong(7, durationSec); + } + if (untilMs == null) { + stmt.setNull(8, java.sql.Types.BIGINT); + } else { + stmt.setLong(8, untilMs); + } + stmt.executeUpdate(); + try (ResultSet keys = stmt.getGeneratedKeys()) { + if (keys.next()) { + return keys.getLong(1); + } + } + } catch (SQLException e) { + logger.error("Ошибка записи moderation_cases guild={}", guildId, e); + } + return -1; + } + + @Override + public Optional findById(String guildId, long caseId) { + String sql = + """ + SELECT id, guild_id, action, target_id, moderator_id, reason, created_at, duration_sec, until_ms + FROM moderation_cases WHERE id = ? AND guild_id = ? + """; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setLong(1, caseId); + stmt.setString(2, guildId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(map(rs)); + } + } + } catch (SQLException e) { + logger.error("Ошибка чтения case id={}", caseId, e); + } + return Optional.empty(); + } + + private static CaseEntry map(ResultSet rs) throws SQLException { + Long duration = rs.getObject("duration_sec") == null ? null : rs.getLong("duration_sec"); + Long until = rs.getObject("until_ms") == null ? null : rs.getLong("until_ms"); + return new CaseEntry( + rs.getLong("id"), + rs.getString("guild_id"), + rs.getString("action"), + rs.getString("target_id"), + rs.getString("moderator_id"), + rs.getString("reason"), + rs.getLong("created_at"), + duration, + until); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/repository/SqlWarningRepository.java b/src/main/java/creator/loki/feature/moderation/repository/SqlWarningRepository.java new file mode 100644 index 0000000..296d30d --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/repository/SqlWarningRepository.java @@ -0,0 +1,139 @@ +package creator.loki.feature.moderation.repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import creator.loki.database.Database; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SqlWarningRepository implements WarningRepository { + private static final Logger logger = LoggerFactory.getLogger(SqlWarningRepository.class); + private final Database database; + + public SqlWarningRepository(Database database) { + this.database = database; + } + + @Override + public long insert(String guildId, String userId, String moderatorId, String reason) { + String sql = + """ + INSERT INTO warnings (guild_id, user_id, moderator_id, reason, created_at) + VALUES (?, ?, ?, ?, ?) + """; + long now = System.currentTimeMillis(); + try (Connection conn = database.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + stmt.setString(3, moderatorId); + stmt.setString(4, reason); + stmt.setLong(5, now); + stmt.executeUpdate(); + try (ResultSet keys = stmt.getGeneratedKeys()) { + if (keys.next()) { + return keys.getLong(1); + } + } + } catch (SQLException e) { + logger.error("Ошибка записи предупреждения guild={} user={}", guildId, userId, e); + } + return -1; + } + + @Override + public List listByUser(String guildId, String userId, int limit) { + int lim = Math.min(Math.max(limit, 1), 25); + String sql = + """ + SELECT id, guild_id, user_id, moderator_id, reason, created_at + FROM warnings + WHERE guild_id = ? AND user_id = ? + ORDER BY id DESC + LIMIT ? + """; + List list = new ArrayList<>(); + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + stmt.setInt(3, lim); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + list.add(map(rs)); + } + } + } catch (SQLException e) { + logger.error("Ошибка чтения предупреждений guild={} user={}", guildId, userId, e); + } + return Collections.unmodifiableList(list); + } + + @Override + public int countByUser(String guildId, String userId) { + String sql = "SELECT COUNT(*) FROM warnings WHERE guild_id = ? AND user_id = ?"; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, userId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } catch (SQLException e) { + logger.error("Ошибка подсчёта предупреждений", e); + } + return 0; + } + + @Override + public boolean deleteById(String guildId, long warningId) { + String sql = "DELETE FROM warnings WHERE id = ? AND guild_id = ?"; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setLong(1, warningId); + stmt.setString(2, guildId); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + logger.error("Ошибка удаления предупреждения id={}", warningId, e); + } + return false; + } + + @Override + public Optional findById(String guildId, long warningId) { + String sql = + """ + SELECT id, guild_id, user_id, moderator_id, reason, created_at + FROM warnings WHERE id = ? AND guild_id = ? + """; + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setLong(1, warningId); + stmt.setString(2, guildId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(map(rs)); + } + } + } catch (SQLException e) { + logger.error("Ошибка поиска предупреждения id={}", warningId, e); + } + return Optional.empty(); + } + + private static WarningEntry map(ResultSet rs) throws SQLException { + return new WarningEntry( + rs.getLong("id"), + rs.getString("guild_id"), + rs.getString("user_id"), + rs.getString("moderator_id"), + rs.getString("reason"), + rs.getLong("created_at")); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/repository/WarningRepository.java b/src/main/java/creator/loki/feature/moderation/repository/WarningRepository.java index dd8f88e..f95c22f 100644 --- a/src/main/java/creator/loki/feature/moderation/repository/WarningRepository.java +++ b/src/main/java/creator/loki/feature/moderation/repository/WarningRepository.java @@ -1,141 +1,19 @@ package creator.loki.feature.moderation.repository; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; -import creator.loki.database.Database; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +public interface WarningRepository { + long insert(String guildId, String userId, String moderatorId, String reason); -/** Предупреждения модераторов по гильдии (SQLite). */ -public class WarningRepository { - private static final Logger logger = LoggerFactory.getLogger(WarningRepository.class); - private final Database database; + List listByUser(String guildId, String userId, int limit); - public WarningRepository(Database database) { - this.database = database; - } + int countByUser(String guildId, String userId); - public long insert(String guildId, String userId, String moderatorId, String reason) { - String sql = - """ - INSERT INTO warnings (guild_id, user_id, moderator_id, reason, created_at) - VALUES (?, ?, ?, ?, ?) - """; - long now = System.currentTimeMillis(); - try (Connection conn = database.getConnection(); - PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - stmt.setString(3, moderatorId); - stmt.setString(4, reason); - stmt.setLong(5, now); - stmt.executeUpdate(); - try (ResultSet keys = stmt.getGeneratedKeys()) { - if (keys.next()) { - return keys.getLong(1); - } - } - } catch (SQLException e) { - logger.error("Ошибка записи предупреждения guild={} user={}", guildId, userId, e); - } - return -1; - } + boolean deleteById(String guildId, long warningId); - public List listByUser(String guildId, String userId, int limit) { - int lim = Math.min(Math.max(limit, 1), 25); - String sql = - """ - SELECT id, guild_id, user_id, moderator_id, reason, created_at - FROM warnings - WHERE guild_id = ? AND user_id = ? - ORDER BY id DESC - LIMIT ? - """; - List list = new ArrayList<>(); - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - stmt.setInt(3, lim); - try (ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - list.add(map(rs)); - } - } - } catch (SQLException e) { - logger.error("Ошибка чтения предупреждений guild={} user={}", guildId, userId, e); - } - return Collections.unmodifiableList(list); - } + Optional findById(String guildId, long warningId); - public int countByUser(String guildId, String userId) { - String sql = "SELECT COUNT(*) FROM warnings WHERE guild_id = ? AND user_id = ?"; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } - } catch (SQLException e) { - logger.error("Ошибка подсчёта предупреждений", e); - } - return 0; - } - - /** - * @return true если строка с таким id и guild удалена - */ - public boolean deleteById(String guildId, long warningId) { - String sql = "DELETE FROM warnings WHERE id = ? AND guild_id = ?"; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setLong(1, warningId); - stmt.setString(2, guildId); - return stmt.executeUpdate() > 0; - } catch (SQLException e) { - logger.error("Ошибка удаления предупреждения id={}", warningId, e); - } - return false; - } - - public Optional findById(String guildId, long warningId) { - String sql = - """ - SELECT id, guild_id, user_id, moderator_id, reason, created_at - FROM warnings WHERE id = ? AND guild_id = ? - """; - try (PreparedStatement stmt = database.prepare(sql)) { - stmt.setLong(1, warningId); - stmt.setString(2, guildId); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - return Optional.of(map(rs)); - } - } - } catch (SQLException e) { - logger.error("Ошибка поиска предупреждения id={}", warningId, e); - } - return Optional.empty(); - } - - private static WarningEntry map(ResultSet rs) throws SQLException { - return new WarningEntry( - rs.getLong("id"), - rs.getString("guild_id"), - rs.getString("user_id"), - rs.getString("moderator_id"), - rs.getString("reason"), - rs.getLong("created_at")); - } - - public record WarningEntry( + record WarningEntry( long id, String guildId, String userId, String moderatorId, String reason, long createdAt) {} } diff --git a/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java b/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java index 14dddf9..09b6ca9 100644 --- a/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java +++ b/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java @@ -10,6 +10,8 @@ import java.util.Optional; import creator.loki.database.Database; import creator.loki.database.DatabaseSchema; import creator.loki.feature.moderation.repository.ActiveMuteRepository.ActiveMuteEntry; + +import creator.loki.feature.moderation.repository.SqlActiveMuteRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -25,7 +27,7 @@ class ActiveMuteRepositoryTest { database = new Database(dbPath); database.connect(); new DatabaseSchema(database).createTables(); - repository = new ActiveMuteRepository(database); + repository = new SqlActiveMuteRepository(database); } @AfterEach diff --git a/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java b/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java index b4fb252..680033d 100644 --- a/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java +++ b/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java @@ -10,6 +10,8 @@ import java.util.Optional; import creator.loki.database.Database; import creator.loki.database.DatabaseSchema; import creator.loki.feature.moderation.repository.WarningRepository.WarningEntry; + +import creator.loki.feature.moderation.repository.SqlWarningRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -25,7 +27,7 @@ class WarningRepositoryTest { database = new Database(dbPath); database.connect(); new DatabaseSchema(database).createTables(); - repository = new WarningRepository(database); + repository = new SqlWarningRepository(database); } @AfterEach diff --git a/src/test/java/creator/loki/feature/radio/RadioManagerTest.java b/src/test/java/creator/loki/feature/radio/RadioManagerTest.java new file mode 100644 index 0000000..c052515 --- /dev/null +++ b/src/test/java/creator/loki/feature/radio/RadioManagerTest.java @@ -0,0 +1,77 @@ +package creator.loki.feature.radio; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import creator.loki.feature.music.repository.QueueRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class RadioManagerTest { + + QueueRepository queueRepository; + RadioManager radioManager; + + @BeforeEach + void setUp() { + queueRepository = mock(QueueRepository.class); + radioManager = new RadioManager(queueRepository); + } + + @Test + void setRadio_storesUrl() { + var guildId = 123L; + var url = "http://stream.example.com/radio"; + + radioManager.setRadio(guildId, url); + + assertEquals(url, radioManager.getRadioUrl(guildId)); + } + + @Test + void getRadioUrl_returnsNullForUnknown() { + assertNull(radioManager.getRadioUrl(999L)); + } + + @Test + void startRadio_returnsEarlyWithoutUrl() { + radioManager.startRadio(123L, 456L); + } + + @Test + void startRadio_returnsEarlyWithoutJda() { + radioManager.setRadio(123L, "http://stream.example.com/radio"); + radioManager.startRadio(123L, 456L); + } + + @Test + void isRadioActive_returnsTrueAfterSet() { + radioManager.setRadio(123L, "http://stream.example.com/radio"); + + assertTrue(radioManager.isRadioActive(123L)); + } + + @Test + void isRadioActive_returnsFalseForUnknown() { + assertFalse(radioManager.isRadioActive(999L)); + } + + @Test + void getVolume_default100() { + assertEquals(100, radioManager.getVolume(999L)); + } + + @Test + void setVolume_clampsRange() { + radioManager.setRadio(123L, "http://stream.example.com/radio"); + + radioManager.setVolume(123L, -50); + assertEquals(0, radioManager.getVolume(123L)); + + radioManager.setVolume(123L, 200); + assertEquals(100, radioManager.getVolume(123L)); + + radioManager.setVolume(123L, 75); + assertEquals(75, radioManager.getVolume(123L)); + } +} diff --git a/src/test/java/creator/loki/feature/role/CustomRoleManagerTest.java b/src/test/java/creator/loki/feature/role/CustomRoleManagerTest.java index 4931b16..075cca8 100644 --- a/src/test/java/creator/loki/feature/role/CustomRoleManagerTest.java +++ b/src/test/java/creator/loki/feature/role/CustomRoleManagerTest.java @@ -9,15 +9,10 @@ import creator.loki.feature.role.repository.CustomRoleRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) class CustomRoleManagerTest { - @Mock private CustomRoleRepository repository; - + private final CustomRoleRepository repository = mock(CustomRoleRepository.class); private CustomRoleManager manager; @BeforeEach @@ -27,10 +22,10 @@ class CustomRoleManagerTest { @Test void getUserRoleIdsDelegatesToRepository() { - List expected = List.of("role-1", "role-2"); + var expected = List.of("role-1", "role-2"); when(repository.getUserRoles("guild-1", "user-1")).thenReturn(expected); - List result = manager.getUserRoleIds("guild-1", "user-1"); + var result = manager.getUserRoleIds("guild-1", "user-1"); assertSame(expected, result); } @@ -39,7 +34,7 @@ class CustomRoleManagerTest { void getUserRoleCountDelegatesToRepository() { when(repository.getUserRoleCount("guild-1", "user-1")).thenReturn(3); - int count = manager.getUserRoleCount("guild-1", "user-1"); + var count = manager.getUserRoleCount("guild-1", "user-1"); assertEquals(3, count); } diff --git a/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java b/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java new file mode 100644 index 0000000..81ad7c1 --- /dev/null +++ b/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java @@ -0,0 +1,157 @@ +package creator.loki.feature.social.commands; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.List; + +import creator.loki.command.CommandContext; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Mentions; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import org.junit.jupiter.api.Test; + +class SocialCommandTest { + + @Test + void hugFlavor_sendsEmbed() { + var cooldownManager = mock(CooldownManager.class); + var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager); + var channel = mock(MessageChannel.class); + var message = mock(Message.class); + var mentions = mock(Mentions.class); + var author = mock(Member.class); + var target = mock(Member.class); + var sendAction = mock(MessageCreateAction.class); + + when(author.getId()).thenReturn("123"); + when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false); + when(message.getMentions()).thenReturn(mentions); + when(mentions.getMembers()).thenReturn(List.of(target)); + when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendAction); + + var context = new CommandContext(message, List.of(), channel, author, null); + + try (var mockedNekos = mockStatic(NekosLifeApi.class)) { + mockedNekos.when(() -> NekosLifeApi.fetchImage(anyString())).thenReturn("https://example.com/hug.gif"); + + command.execute(context); + + mockedNekos.verify(() -> NekosLifeApi.fetchImage(eq("hug"))); + verify(cooldownManager).applyCooldown(eq("123"), eq("hug"), eq(5)); + verify(channel).sendMessageEmbeds(any(MessageEmbed.class)); + verify(sendAction).queue(); + } + } + + @Test + void noMention_sendsError() { + var cooldownManager = mock(CooldownManager.class); + var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager); + var channel = mock(MessageChannel.class); + var message = mock(Message.class); + var mentions = mock(Mentions.class); + var author = mock(Member.class); + var sendAction = mock(MessageCreateAction.class); + + when(author.getId()).thenReturn("123"); + when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false); + when(message.getMentions()).thenReturn(mentions); + when(mentions.getMembers()).thenReturn(List.of()); + when(channel.sendMessage(anyString())).thenReturn(sendAction); + + var context = new CommandContext(message, List.of(), channel, author, null); + + command.execute(context); + + verify(channel).sendMessage("❌ Укажи пользователя через @"); + verify(sendAction).queue(); + } + + @Test + void onCooldown_showsRemaining() { + var cooldownManager = mock(CooldownManager.class); + var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager); + var channel = mock(MessageChannel.class); + var message = mock(Message.class); + var author = mock(Member.class); + var sendAction = mock(MessageCreateAction.class); + + when(author.getId()).thenReturn("123"); + when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(true); + when(cooldownManager.getRemainingSeconds(anyString(), anyString())).thenReturn(42L); + when(channel.sendMessage(anyString())).thenReturn(sendAction); + + var context = new CommandContext(message, List.of(), channel, author, null); + + command.execute(context); + + verify(channel).sendMessage("⏰ Подожди ещё 42 сек"); + verify(sendAction).queue(); + } + + @Test + void kissFlavor_rejectsSelf() { + var cooldownManager = mock(CooldownManager.class); + var command = new SocialCommand(SocialCommand.Flavor.KISS, cooldownManager); + var channel = mock(MessageChannel.class); + var message = mock(Message.class); + var mentions = mock(Mentions.class); + var author = mock(Member.class); + var target = mock(Member.class); + var sendAction = mock(MessageCreateAction.class); + + when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false); + when(message.getMentions()).thenReturn(mentions); + when(mentions.getMembers()).thenReturn(List.of(target)); + when(author.getId()).thenReturn("123"); + when(target.getId()).thenReturn("123"); + when(channel.sendMessage(anyString())).thenReturn(sendAction); + + var context = new CommandContext(message, List.of(), channel, author, null); + + command.execute(context); + + verify(channel).sendMessage("❌ Нельзя использовать на себе!"); + verify(sendAction).queue(); + } + + @Test + void kissFlavor_rejectsBot() { + var cooldownManager = mock(CooldownManager.class); + var command = new SocialCommand(SocialCommand.Flavor.KISS, cooldownManager); + var channel = mock(MessageChannel.class); + var message = mock(Message.class); + var mentions = mock(Mentions.class); + var author = mock(Member.class); + var target = mock(Member.class); + var targetUser = mock(User.class); + var sendAction = mock(MessageCreateAction.class); + + when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false); + when(message.getMentions()).thenReturn(mentions); + when(mentions.getMembers()).thenReturn(List.of(target)); + when(author.getId()).thenReturn("123"); + when(target.getId()).thenReturn("456"); + when(target.getUser()).thenReturn(targetUser); + when(targetUser.isBot()).thenReturn(true); + when(channel.sendMessage(anyString())).thenReturn(sendAction); + + var context = new CommandContext(message, List.of(), channel, author, null); + + command.execute(context); + + verify(channel).sendMessage("❌ Боты не поддерживают это действие!"); + verify(sendAction).queue(); + } +} diff --git a/src/test/java/creator/loki/feature/voice/handler/VoiceButtonHandlerTest.java b/src/test/java/creator/loki/feature/voice/handler/VoiceButtonHandlerTest.java new file mode 100644 index 0000000..4f04aac --- /dev/null +++ b/src/test/java/creator/loki/feature/voice/handler/VoiceButtonHandlerTest.java @@ -0,0 +1,17 @@ +package creator.loki.feature.voice.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import creator.loki.feature.voice.service.VoiceChannelManager; +import org.junit.jupiter.api.Test; + +class VoiceButtonHandlerTest { + + @Test + void constructorAcceptsVoiceManager() { + var manager = mock(VoiceChannelManager.class); + var handler = new VoiceButtonHandler(manager); + assertNotNull(handler); + } +}