diff --git a/src/main/java/creator/loki/database/DatabaseMigration.java b/src/main/java/creator/loki/database/DatabaseMigration.java index 2889c10..0b02dfc 100644 --- a/src/main/java/creator/loki/database/DatabaseMigration.java +++ b/src/main/java/creator/loki/database/DatabaseMigration.java @@ -1,11 +1,9 @@ package creator.loki.database; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.FileInputStream; import java.io.InputStreamReader; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; @@ -15,7 +13,6 @@ import creator.loki.util.LoggerDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Миграция данных из JSON в SQLite */ public class DatabaseMigration { private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class); private final Database database; @@ -25,80 +22,51 @@ public class DatabaseMigration { this.database = database; } - /** Мигрировать данные из JSON */ public void migrateFromJson(String jsonPath) { LoggerDecorator.database(logger, "Начало миграции данных из JSON: " + jsonPath); - if (!new java.io.File(jsonPath).exists()) { logger.info("No legacy data file found at {}, skipping migration", jsonPath); return; } - - try (InputStreamReader reader = - new InputStreamReader(new FileInputStream(jsonPath), StandardCharsets.UTF_8)) { - Type type = new TypeToken>() {}.getType(); - Map data = gson.fromJson(reader, type); - - if (data == null) { - LoggerDecorator.warn(logger, "JSON файл пустой или не существует"); - return; - } - - if (isMigrated()) { - LoggerDecorator.database(logger, "Данные уже мигрированы, пропускаем"); - return; - } - - migrateUserStats(data); - migrateReputation(data); - migrateShipPairs(data); - migrateCustomRoles(data); - markMigrated(); - - LoggerDecorator.success(logger, "Миграция завершена успешно"); - - } catch (Exception e) { - LoggerDecorator.error(logger, "Ошибка миграции данных", e); + var data = parseJson(jsonPath); + if (data == null) return; + try (var chk = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) { + var rs = chk.executeQuery(); + if (rs.next() && "true".equals(rs.getString("value"))) return; + } catch (SQLException ignored) { } - } - - private boolean isMigrated() { - try (PreparedStatement stmt = - database.prepare("SELECT value FROM config WHERE key = 'migrated'")) { - ResultSet rs = stmt.executeQuery(); - return rs.next() && "true".equals(rs.getString("value")); - } catch (SQLException e) { - return false; - } - } - - private void markMigrated() { - try (PreparedStatement stmt = - database.prepare( - "INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')")) { - stmt.executeUpdate(); + migrateUserStats(data); + migrateReputation(data); + migrateShipPairs(data); + migrateCustomRoles(data); + var sql = "INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"; + try (var p = database.prepare(sql)) { + p.executeUpdate(); } catch (SQLException e) { LoggerDecorator.error(logger, "Ошибка сохранения статуса миграции", e); } + LoggerDecorator.success(logger, "Миграция завершена успешно"); + } + + private Map parseJson(String path) { + try (var reader = new InputStreamReader(new FileInputStream(path), UTF_8)) { + var type = new TypeToken>() {}.getType(); + return (Map) gson.fromJson(reader, type); + } catch (Exception e) { + LoggerDecorator.error(logger, "Ошибка миграции данных", e); + return null; + } } - @SuppressWarnings("unchecked") private void migrateUserStats(Map data) { - Map userStats = (Map) data.get("userStats"); + var userStats = (Map) data.get("userStats"); if (userStats == null) return; - LoggerDecorator.database(logger, "Миграция userStats: " + userStats.size() + " записей"); - - String sql = - """ - INSERT OR REPLACE INTO user_stats - (user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join) - VALUES (?, ?, ?, ?, ?, ?, ?)"""; - - try (PreparedStatement stmt = database.prepare(sql)) { - for (Map.Entry entry : userStats.entrySet()) { - Map stats = (Map) entry.getValue(); - + try (var stmt = + database.prepare( + "INSERT OR REPLACE INTO user_stats (user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join) VALUES (?, ?, ?, ?, ?, ?, ?)")) { + for (var entry : userStats.entrySet()) { + var stats = (Map) entry.getValue(); stmt.setString(1, (String) stats.get("userId")); stmt.setInt(2, ((Number) stats.get("level")).intValue()); stmt.setInt(3, ((Number) stats.get("totalXP")).intValue()); @@ -106,7 +74,6 @@ public class DatabaseMigration { stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue()); stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue()); stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue()); - stmt.executeUpdate(); } LoggerDecorator.success(logger, "userStats мигрированы"); @@ -115,19 +82,16 @@ public class DatabaseMigration { } } - @SuppressWarnings("unchecked") private void migrateReputation(Map data) { - Map reputation = (Map) data.get("reputation"); + var reputation = (Map) data.get("reputation"); if (reputation == null) return; - LoggerDecorator.database(logger, "Миграция reputation: " + reputation.size() + " записей"); - - try (PreparedStatement stmt = + try (var a = database.prepare("INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)")) { - for (Map.Entry entry : reputation.entrySet()) { - stmt.setString(1, entry.getKey()); - stmt.setDouble(2, ((Number) entry.getValue()).doubleValue()); - stmt.executeUpdate(); + for (var entry : reputation.entrySet()) { + a.setString(1, entry.getKey()); + a.setDouble(2, ((Number) entry.getValue()).doubleValue()); + a.executeUpdate(); } LoggerDecorator.success(logger, "reputation мигрирована"); } catch (SQLException e) { @@ -135,23 +99,20 @@ public class DatabaseMigration { } } - @SuppressWarnings("unchecked") private void migrateShipPairs(Map data) { - Map shipPairs = (Map) data.get("shipPairs"); + var shipPairs = (Map) data.get("shipPairs"); if (shipPairs == null) return; - LoggerDecorator.database(logger, "Миграция shipPairs: " + shipPairs.size() + " записей"); - - try (PreparedStatement stmt = + try (var b = database.prepare( "INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)")) { - for (Map.Entry entry : shipPairs.entrySet()) { - String[] users = entry.getKey().split("-"); + for (var entry : shipPairs.entrySet()) { + var users = entry.getKey().split("-"); if (users.length == 2) { - stmt.setString(1, users[0]); - stmt.setString(2, users[1]); - stmt.setDouble(3, ((Number) entry.getValue()).doubleValue()); - stmt.executeUpdate(); + b.setString(1, users[0]); + b.setString(2, users[1]); + b.setDouble(3, ((Number) entry.getValue()).doubleValue()); + b.executeUpdate(); } } LoggerDecorator.success(logger, "shipPairs мигрированы"); @@ -160,30 +121,22 @@ public class DatabaseMigration { } } - @SuppressWarnings("unchecked") private void migrateCustomRoles(Map data) { - Map customRoles = (Map) data.get("custom_roles"); + var customRoles = (Map) data.get("custom_roles"); if (customRoles == null) return; - LoggerDecorator.database(logger, "Миграция custom_roles"); - - try (PreparedStatement stmt = + try (var c = database.prepare( "INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)")) { - for (Map.Entry guildEntry : customRoles.entrySet()) { - String guildId = guildEntry.getKey(); - Map users = (Map) guildEntry.getValue(); - - for (Map.Entry userEntry : users.entrySet()) { - String userId = userEntry.getKey(); - Object rolesObj = userEntry.getValue(); - - if (rolesObj instanceof java.util.List rolesList) { - for (Object roleId : rolesList) { - stmt.setString(1, guildId); - stmt.setString(2, userId); - stmt.setString(3, roleId.toString()); - stmt.executeUpdate(); + for (var guildEntry : customRoles.entrySet()) { + var users = (Map) guildEntry.getValue(); + for (var userEntry : users.entrySet()) { + if (userEntry.getValue() instanceof java.util.List rolesList) { + for (var roleId : rolesList) { + c.setString(1, guildEntry.getKey()); + c.setString(2, userEntry.getKey()); + c.setString(3, roleId.toString()); + c.executeUpdate(); } } } diff --git a/src/main/java/creator/loki/database/DatabaseModule.java b/src/main/java/creator/loki/database/DatabaseModule.java new file mode 100644 index 0000000..d3845c4 --- /dev/null +++ b/src/main/java/creator/loki/database/DatabaseModule.java @@ -0,0 +1,54 @@ +package creator.loki.database; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import creator.loki.config.BotConfig; +import creator.loki.feature.config.repository.ConfigRepository; +import creator.loki.feature.config.repository.GuildConfigRepository; +import creator.loki.feature.config.repository.PrefixedGuildConfigRepository; +import creator.loki.feature.music.repository.QueueRepository; +import creator.loki.feature.rating.repository.RatingsRepository; +import creator.loki.feature.reputation.repository.ReputationRepository; +import creator.loki.feature.ship.repository.ShipPairRepository; + +public class DatabaseModule extends AbstractModule { + @Provides @Singleton + Database provideDatabase(BotConfig config) { + Database database = new Database(config.getDbPath()); + database.connect(); + new DatabaseSchema(database).createTables(); + new DatabaseMigration(database).migrateFromJson("data/bot_data.json"); + return database; + } + + @Provides @Singleton + ConfigRepository provideConfigRepository(Database database) { + return new ConfigRepository(database); + } + + @Provides @Singleton + GuildConfigRepository provideGuildConfigRepository(ConfigRepository cr) { + return new PrefixedGuildConfigRepository(cr); + } + + @Provides @Singleton + RatingsRepository provideRatingsRepository(Database database) { + return new RatingsRepository(database); + } + + @Provides @Singleton + ReputationRepository provideReputationRepository(Database database) { + return new ReputationRepository(database); + } + + @Provides @Singleton + ShipPairRepository provideShipPairRepository(Database database) { + return new ShipPairRepository(database); + } + + @Provides @Singleton + QueueRepository provideQueueRepository(Database database) { + return new QueueRepository(database); + } +} diff --git a/src/main/java/creator/loki/database/schema/FeatureTables.java b/src/main/java/creator/loki/database/schema/FeatureTables.java index da05589..6d32c4b 100644 --- a/src/main/java/creator/loki/database/schema/FeatureTables.java +++ b/src/main/java/creator/loki/database/schema/FeatureTables.java @@ -3,191 +3,145 @@ package creator.loki.database.schema; import creator.loki.database.Database; public final class FeatureTables { + private static final String ECONOMY = + """ + CREATE TABLE IF NOT EXISTS economy ( + user_id TEXT PRIMARY KEY, + cash INTEGER NOT NULL DEFAULT 0, + bank INTEGER NOT NULL DEFAULT 0, + total_earned INTEGER NOT NULL DEFAULT 0, + total_spent INTEGER NOT NULL DEFAULT 0, + last_daily TEXT, + daily_streak INTEGER DEFAULT 0, + fishing_luck INTEGER DEFAULT 0, + efficiency INTEGER DEFAULT 0, + last_fish TEXT, + job TEXT, + last_work TEXT + )"""; + private static final String MUSIC_QUEUE = + """ + CREATE TABLE IF NOT EXISTS music_queue ( + guild_id TEXT NOT NULL, + position INTEGER NOT NULL, + track_encoded TEXT NOT NULL, + track_title TEXT, + track_author TEXT, + track_uri TEXT, + track_duration BIGINT, + PRIMARY KEY (guild_id, position) + )"""; + private static final String WARNINGS = + """ + CREATE TABLE IF NOT EXISTS warnings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + moderator_id TEXT NOT NULL, + reason TEXT NOT NULL, + created_at INTEGER NOT NULL + )"""; + private static final String MODERATION_CASES = + """ + CREATE TABLE IF NOT EXISTS moderation_cases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + action TEXT NOT NULL, + target_id TEXT NOT NULL, + moderator_id TEXT NOT NULL, + reason TEXT NOT NULL, + created_at INTEGER NOT NULL, + duration_sec INTEGER, + until_ms INTEGER + )"""; + private static final String ACTIVE_MUTES = + """ + CREATE TABLE IF NOT EXISTS active_mutes ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + ends_at_ms INTEGER NOT NULL, + case_id INTEGER NOT NULL, + PRIMARY KEY (guild_id, user_id) + )"""; + private static final String CUSTOM_ROLES = + """ + CREATE TABLE IF NOT EXISTS custom_roles ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + role_id TEXT NOT NULL, + PRIMARY KEY (guild_id, user_id, role_id) + )"""; + private static final String VOICE_CHANNELS = + """ + CREATE TABLE IF NOT EXISTS voice_channels ( + channel_id TEXT PRIMARY KEY, + owner_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + name TEXT, + user_limit INTEGER DEFAULT 0, + bitrate INTEGER DEFAULT 64000, + locked INTEGER DEFAULT 0, + text_channel_id TEXT + )"""; + private static final String VOICE_PRESETS = + """ + CREATE TABLE IF NOT EXISTS voice_presets ( + user_id TEXT PRIMARY KEY, + name TEXT, + user_limit INTEGER DEFAULT 0, + bitrate INTEGER DEFAULT 64000, + region TEXT DEFAULT 'auto', + locked INTEGER DEFAULT 0, + allowed_role_id TEXT, + denied_role_id TEXT + )"""; + private static final String COUNTER_STATS = + """ + CREATE TABLE IF NOT EXISTS counter_stats ( + date TEXT PRIMARY KEY, + joined INTEGER DEFAULT 0, + left_count INTEGER DEFAULT 0, + difference INTEGER DEFAULT 0 + )"""; + private static final String RATINGS = + """ + CREATE TABLE IF NOT EXISTS ratings ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + rating_type TEXT NOT NULL, + value INTEGER NOT NULL, + PRIMARY KEY (guild_id, user_id, rating_type) + )"""; public static void create(Database database) { - createEconomy(database); - createMusicQueue(database); - createWarnings(database); - createModerationCases(database); - createActiveMutes(database); - createCustomRoles(database); - createVoiceChannels(database); - createVoicePresets(database); - createCounterStats(database); - createRatings(database); + database.execute(ECONOMY); + database.execute(MUSIC_QUEUE); + database.execute(WARNINGS); + database.execute(MODERATION_CASES); + database.execute(ACTIVE_MUTES); + database.execute(CUSTOM_ROLES); + database.execute(VOICE_CHANNELS); + database.execute(VOICE_PRESETS); + database.execute(COUNTER_STATS); + database.execute(RATINGS); createIndexes(database); } - private static void createEconomy(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS economy ( - user_id TEXT PRIMARY KEY, - cash INTEGER NOT NULL DEFAULT 0, - bank INTEGER NOT NULL DEFAULT 0, - total_earned INTEGER NOT NULL DEFAULT 0, - total_spent INTEGER NOT NULL DEFAULT 0, - last_daily TEXT, - daily_streak INTEGER DEFAULT 0, - fishing_luck INTEGER DEFAULT 0, - efficiency INTEGER DEFAULT 0, - last_fish TEXT, - job TEXT, - last_work TEXT - ) - """); - } - - private static void createMusicQueue(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS music_queue ( - guild_id TEXT NOT NULL, - position INTEGER NOT NULL, - track_encoded TEXT NOT NULL, - track_title TEXT, - track_author TEXT, - track_uri TEXT, - track_duration BIGINT, - PRIMARY KEY (guild_id, position) - ) - """); - } - - private static void createWarnings(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS warnings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - moderator_id TEXT NOT NULL, - reason TEXT NOT NULL, - created_at INTEGER NOT NULL - ) - """); - } - - private static void createModerationCases(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS moderation_cases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT NOT NULL, - action TEXT NOT NULL, - target_id TEXT NOT NULL, - moderator_id TEXT NOT NULL, - reason TEXT NOT NULL, - created_at INTEGER NOT NULL, - duration_sec INTEGER, - until_ms INTEGER - ) - """); - } - - private static void createActiveMutes(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS active_mutes ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - ends_at_ms INTEGER NOT NULL, - case_id INTEGER NOT NULL, - PRIMARY KEY (guild_id, user_id) - ) - """); - } - - private static void createCustomRoles(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS custom_roles ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - role_id TEXT NOT NULL, - PRIMARY KEY (guild_id, user_id, role_id) - ) - """); - } - - private static void createVoiceChannels(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS voice_channels ( - channel_id TEXT PRIMARY KEY, - owner_id TEXT NOT NULL, - created_at INTEGER NOT NULL, - name TEXT, - user_limit INTEGER DEFAULT 0, - bitrate INTEGER DEFAULT 64000, - locked INTEGER DEFAULT 0, - text_channel_id TEXT - ) - """); - } - - private static void createVoicePresets(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS voice_presets ( - user_id TEXT PRIMARY KEY, - name TEXT, - user_limit INTEGER DEFAULT 0, - bitrate INTEGER DEFAULT 64000, - region TEXT DEFAULT 'auto', - locked INTEGER DEFAULT 0, - allowed_role_id TEXT, - denied_role_id TEXT - ) - """); - } - - private static void createCounterStats(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS counter_stats ( - date TEXT PRIMARY KEY, - joined INTEGER DEFAULT 0, - left_count INTEGER DEFAULT 0, - difference INTEGER DEFAULT 0 - ) - """); - } - - private static void createRatings(Database database) { - database.execute( - """ - CREATE TABLE IF NOT EXISTS ratings ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - rating_type TEXT NOT NULL, - value INTEGER NOT NULL, - PRIMARY KEY (guild_id, user_id, rating_type) - ) - """); - } - private static void createIndexes(Database database) { - database.execute( - "CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)"); - database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)"); - database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)"); - database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)"); - database.execute( - "CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)"); + for (var sql : + new String[] { + "CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)", + "CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)", + "CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)", + "CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)", + "CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)", + "CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)", + "CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)", + "CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)", + "CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)", + "CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)", + "CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)", + "CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)" + }) database.execute(sql); } } diff --git a/src/main/java/creator/loki/di/BotModule.java b/src/main/java/creator/loki/di/BotModule.java index b6288c1..7214942 100644 --- a/src/main/java/creator/loki/di/BotModule.java +++ b/src/main/java/creator/loki/di/BotModule.java @@ -15,12 +15,7 @@ import creator.loki.core.registry.CommandRegistry; import creator.loki.core.registry.PrefixCommandRegistrar; import creator.loki.core.security.CooldownManager; import creator.loki.core.security.RateLimiter; -import creator.loki.database.Database; -import creator.loki.database.DatabaseMigration; -import creator.loki.database.DatabaseSchema; -import creator.loki.feature.config.repository.ConfigRepository; -import creator.loki.feature.config.repository.GuildConfigRepository; -import creator.loki.feature.config.repository.PrefixedGuildConfigRepository; +import creator.loki.database.DatabaseModule; import creator.loki.feature.counter.CounterModule; import creator.loki.feature.economy.EconomyCommandRegistrar; import creator.loki.feature.economy.EconomyModule; @@ -34,20 +29,15 @@ import creator.loki.feature.moderation.ModerationCommandRegistrar; import creator.loki.feature.moderation.ModerationModule; import creator.loki.feature.music.MusicCommandRegistrar; import creator.loki.feature.music.MusicModule; -import creator.loki.feature.music.repository.QueueRepository; import creator.loki.feature.profile.ProfileModule; import creator.loki.feature.radio.RadioCommandRegistrar; import creator.loki.feature.radio.RadioModule; -import creator.loki.feature.rating.repository.RatingsRepository; -import creator.loki.feature.reputation.repository.ReputationRepository; import creator.loki.feature.role.RoleModule; -import creator.loki.feature.ship.repository.ShipPairRepository; import creator.loki.feature.utility.UtilityCommandRegistrar; import creator.loki.feature.voice.VoiceModule; import creator.loki.feature.welcome.WelcomeModule; public class BotModule extends AbstractModule { - @Override protected void configure() { bind(CommandRegistry.class).in(Singleton.class); @@ -60,6 +50,7 @@ public class BotModule extends AbstractModule { bind(ShutdownManager.class).in(Singleton.class); bind(PrefixCommandRegistrar.class).in(Singleton.class); + install(new DatabaseModule()); install(new VoiceModule()); install(new MusicModule()); install(new LevelModule()); @@ -83,67 +74,12 @@ public class BotModule extends AbstractModule { commandBinder.addBinding().to(RadioCommandRegistrar.class); } - @Provides - @Singleton - BotConfig provideBotConfig() { - return ConfigLoader.load(); - } + @Provides @Singleton + BotConfig provideBotConfig() { return ConfigLoader.load(); } - @Provides - @Singleton + @Provides @Singleton EventDispatcher provideEventDispatcher( - BotConfig config, - CommandRegistry registry, - RateLimiter rateLimiter, - MetricsCollector metrics) { + BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) { return new EventDispatcher(config, registry, rateLimiter, metrics); } - - @Provides - @Singleton - Database provideDatabase(BotConfig config) { - Database database = new Database(config.getDbPath()); - database.connect(); - DatabaseSchema schema = new DatabaseSchema(database); - schema.createTables(); - DatabaseMigration migration = new DatabaseMigration(database); - migration.migrateFromJson("data/bot_data.json"); - return database; - } - - @Provides - @Singleton - ConfigRepository provideConfigRepository(Database database) { - return new ConfigRepository(database); - } - - @Provides - @Singleton - GuildConfigRepository provideGuildConfigRepository(ConfigRepository configRepository) { - return new PrefixedGuildConfigRepository(configRepository); - } - - @Provides - @Singleton - RatingsRepository provideRatingsRepository(Database database) { - return new RatingsRepository(database); - } - - @Provides - @Singleton - ReputationRepository provideReputationRepository(Database database) { - return new ReputationRepository(database); - } - - @Provides - @Singleton - ShipPairRepository provideShipPairRepository(Database database) { - return new ShipPairRepository(database); - } - - @Provides - @Singleton - QueueRepository provideQueueRepository(Database database) { - return new QueueRepository(database); - } } diff --git a/src/main/java/creator/loki/feature/economy/service/BlackjackService.java b/src/main/java/creator/loki/feature/economy/service/BlackjackService.java index 9d2d484..5b31817 100644 --- a/src/main/java/creator/loki/feature/economy/service/BlackjackService.java +++ b/src/main/java/creator/loki/feature/economy/service/BlackjackService.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; -import creator.loki.feature.economy.domain.EconomyAccount; import creator.loki.feature.economy.repository.EconomyRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,109 +29,72 @@ public class BlackjackService { public BlackjackState startGame(String userId, long bet) { if (bet <= 0 || activeGames.containsKey(userId)) return null; - EconomyAccount account = repository.getAccount(userId); + var account = repository.getAccount(userId); if (account.getCash() < bet) return null; repository.save(account.withCash(account.getCash() - bet)); - List playerCards = new ArrayList<>(List.of(drawCard(), drawCard())); - List dealerCards = new ArrayList<>(List.of(drawCard(), drawCard())); + var playerCards = new ArrayList<>(List.of(drawCard(), drawCard())); + var dealerCards = new ArrayList<>(List.of(drawCard(), drawCard())); - int playerSum = calculateSum(playerCards); - int dealerSum = calculateSum(dealerCards); - boolean playerBj = playerSum == 21; - boolean dealerBj = dealerSum == 21; + var playerSum = calculateSum(playerCards); + var dealerSum = calculateSum(dealerCards); + var playerBj = playerSum == 21; + var dealerBj = dealerSum == 21; if (playerBj || dealerBj) { - long payout; - if (playerBj && !dealerBj) { - payout = (long) (bet * 2.5); - } else { - payout = 0; - } - repository.save( - repository.getAccount(userId).withCash(repository.getAccount(userId).getCash() + payout)); - return new BlackjackState( - playerCards.toArray(String[]::new), - dealerCards.toArray(String[]::new), - playerSum, - dealerSum, - bet, - true, - payout); + var payout = playerBj && !dealerBj ? (long) (bet * 2.5) : 0; + creditUser(userId, payout); + return buildState(playerCards, dealerCards, playerSum, dealerSum, bet, true, payout); } - ActiveBlackjack game = new ActiveBlackjack(playerCards, dealerCards, bet, userId); - activeGames.put(userId, game); - return new BlackjackState( - playerCards.toArray(String[]::new), - dealerCards.toArray(String[]::new), - playerSum, - dealerSum, - bet, - false, - 0); + activeGames.put(userId, new ActiveBlackjack(playerCards, dealerCards, bet, userId)); + return buildState(playerCards, dealerCards, playerSum, dealerSum, bet, false, 0); } public BlackjackState hit(String userId) { - ActiveBlackjack game = activeGames.get(userId); + var game = activeGames.get(userId); if (game == null) return null; game.playerCards().add(drawCard()); - int sum = calculateSum(game.playerCards()); + var sum = calculateSum(game.playerCards()); - if (sum > 21) { - activeGames.remove(userId); - return new BlackjackState( - game.playerCards().toArray(String[]::new), - game.dealerCards().toArray(String[]::new), - sum, - calculateSum(game.dealerCards()), - game.bet(), - true, - 0); - } + var over = sum > 21; + if (over) activeGames.remove(userId); - return new BlackjackState( - game.playerCards().toArray(String[]::new), - game.dealerCards().toArray(String[]::new), + return buildState( + game.playerCards(), + game.dealerCards(), sum, calculateSum(game.dealerCards()), game.bet(), - false, + over, 0); } public BlackjackState stand(String userId) { - ActiveBlackjack game = activeGames.remove(userId); + var game = activeGames.remove(userId); if (game == null) return null; - List dealerCards = new ArrayList<>(game.dealerCards()); + var dealerCards = new ArrayList<>(game.dealerCards()); while (calculateSum(dealerCards) < 16) { dealerCards.add(drawCard()); } - int playerSum = calculateSum(game.playerCards()); - int dealerSum = calculateSum(dealerCards); - long payout; + var playerSum = calculateSum(game.playerCards()); + var dealerSum = calculateSum(dealerCards); + var payout = dealerSum > 21 || playerSum > dealerSum ? game.bet() * 2 : 0; - if (dealerSum > 21 || playerSum > dealerSum) { - payout = game.bet() * 2; - } else { - payout = 0; - } + creditUser(userId, payout); - EconomyAccount account = repository.getAccount(userId); - repository.save(account.withCash(account.getCash() + payout)); + return buildState( + game.playerCards(), dealerCards, playerSum, dealerSum, game.bet(), true, payout); + } - return new BlackjackState( - game.playerCards().toArray(String[]::new), - dealerCards.toArray(String[]::new), - playerSum, - dealerSum, - game.bet(), - true, - payout); + private void creditUser(String userId, long amount) { + if (amount <= 0) return; + var account = repository.getAccount(userId); + repository.save(account.withCash(account.getCash() + amount)); } private String drawCard() { @@ -140,9 +102,9 @@ public class BlackjackService { } private int calculateSum(List cards) { - int sum = 0; - int aces = 0; - for (String c : cards) { + var sum = 0; + var aces = 0; + for (var c : cards) { switch (c) { case "J", "Q", "K" -> sum += 10; case "A" -> { @@ -159,6 +121,24 @@ public class BlackjackService { return sum; } + private BlackjackState buildState( + List playerCards, + List dealerCards, + int playerSum, + int dealerSum, + long bet, + boolean isOver, + long payout) { + return new BlackjackState( + playerCards.toArray(String[]::new), + dealerCards.toArray(String[]::new), + playerSum, + dealerSum, + bet, + isOver, + payout); + } + public record BlackjackState( String[] playerCards, String[] dealerCards, diff --git a/src/main/java/creator/loki/feature/level/MessageListener.java b/src/main/java/creator/loki/feature/level/MessageListener.java index 23c07c4..1839551 100644 --- a/src/main/java/creator/loki/feature/level/MessageListener.java +++ b/src/main/java/creator/loki/feature/level/MessageListener.java @@ -23,8 +23,10 @@ public class MessageListener extends ListenerAdapter { @Inject public MessageListener( - EventDispatcher dispatcher, LevelService levelService, - EconomyService economyService, LevelUpNotifier levelUpNotifier) { + EventDispatcher dispatcher, + LevelService levelService, + EconomyService economyService, + LevelUpNotifier levelUpNotifier) { this.dispatcher = dispatcher; this.levelService = levelService; this.economyService = economyService; @@ -39,8 +41,7 @@ public class MessageListener extends ListenerAdapter { LevelUpResult levelUp = levelService.addMessageXP(userId); if (levelUp != null && event.getMember() != null) { levelUpNotifier.sendLevelUpNotification( - event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(), - event.getChannel()); + event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(), event.getChannel()); } int lcAmount = ThreadLocalRandom.current().nextInt(1, 4); economyService.earnCash(userId, lcAmount); diff --git a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java index c48a71f..dbeac8a 100644 --- a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java +++ b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java @@ -32,38 +32,53 @@ public class ResetLevelsButtonHandler extends ListenerAdapter { String authorId = buttonId.substring(PREFIX_CONFIRM.length()); if (authorId.isEmpty()) return; if (!event.getUser().getId().equals(authorId)) { - event.reply("❌ Только инициатор команды может подтвердить действие!").setEphemeral(true).queue(); + event + .reply("❌ Только инициатор команды может подтвердить действие!") + .setEphemeral(true) + .queue(); return; } event.deferReply().queue(); try { resetCommand.executeReset(event.getGuild().getId(), event.getUser().getId()); event.getMessage().delete().queue(); - event.getHook().sendMessageEmbeds( - EmbedFactory.create() - .withTitle("✅ Сброс уровней выполнен") - .withDescription("Уровни и XP всех пользователей успешно сброшены!\n\n" - + "**Сохранено:**\n• Время в голосовых каналах\n" - + "• Количество сообщений\n• Репутация\n\n" - + "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n" - + "Теперь используется новая система уровней!") - .withColor(Color.GREEN) - .withFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl()) - .build()).queue(); + event + .getHook() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("✅ Сброс уровней выполнен") + .withDescription( + "Уровни и XP всех пользователей успешно сброшены!\n\n" + + "**Сохранено:**\n• Время в голосовых каналах\n" + + "• Количество сообщений\n• Репутация\n\n" + + "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n" + + "Теперь используется новая система уровней!") + .withColor(Color.GREEN) + .withFooter( + "Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl()) + .build()) + .queue(); LoggerDecorator.bot(logger, "Сброс уровней подтверждён: " + event.getUser().getName()); } catch (Exception e) { LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e); - event.getHook().sendMessageEmbeds( - EmbedFactory.create() - .withTitle("❌ Ошибка") - .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) - .withColor(Color.RED).build()).queue(); + event + .getHook() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("❌ Ошибка") + .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) + .withColor(Color.RED) + .build()) + .queue(); } } else if (buttonId.startsWith(PREFIX_CANCEL)) { String authorId = buttonId.substring(PREFIX_CANCEL.length()); if (authorId.isEmpty()) return; if (!event.getUser().getId().equals(authorId)) { - event.reply("❌ Только инициатор команды может отменить действие!").setEphemeral(true).queue(); + event + .reply("❌ Только инициатор команды может отменить действие!") + .setEphemeral(true) + .queue(); return; } event.getMessage().delete().queue(); diff --git a/src/main/java/creator/loki/feature/level/VoiceTracker.java b/src/main/java/creator/loki/feature/level/VoiceTracker.java index f8be26a..0db57ca 100644 --- a/src/main/java/creator/loki/feature/level/VoiceTracker.java +++ b/src/main/java/creator/loki/feature/level/VoiceTracker.java @@ -28,8 +28,8 @@ public class VoiceTracker extends ListenerAdapter { private final Object lock = new Object(); @Inject - public VoiceTracker(LevelService levelService, EconomyService economyService, - LevelUpNotifier levelUpNotifier) { + public VoiceTracker( + LevelService levelService, EconomyService economyService, LevelUpNotifier levelUpNotifier) { this.levelService = levelService; this.economyService = economyService; this.levelUpNotifier = levelUpNotifier; @@ -47,7 +47,9 @@ public class VoiceTracker extends ListenerAdapter { voiceJoinTimes.put(userId, System.currentTimeMillis()); } else if (event.getChannelLeft() != null && event.getChannelJoined() == null) { Long joinTime; - synchronized (lock) { joinTime = voiceJoinTimes.remove(userId); } + synchronized (lock) { + joinTime = voiceJoinTimes.remove(userId); + } if (joinTime != null) { long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - joinTime); if (minutes > 0) { @@ -55,13 +57,17 @@ public class VoiceTracker extends ListenerAdapter { economyService.earnCash(userId, minutes / 2); if (lr != null && event.getMember() != null) { levelUpNotifier.sendLevelUpNotification( - event.getMember(), lr.getOldLevel(), lr.getNewLevel(), + event.getMember(), + lr.getOldLevel(), + lr.getNewLevel(), event.getGuild().getSystemChannel()); } } } } else if (event.getChannelJoined() != null && event.getChannelLeft() != null) { - synchronized (lock) { voiceJoinTimes.put(userId, System.currentTimeMillis()); } + synchronized (lock) { + voiceJoinTimes.put(userId, System.currentTimeMillis()); + } } } @@ -82,7 +88,11 @@ public class VoiceTracker extends ListenerAdapter { public void shutdown() { scheduler.shutdown(); - try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); } - catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } } } diff --git a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java index 3c27581..2d312a0 100644 --- a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java +++ b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java @@ -3,7 +3,6 @@ package creator.loki.feature.level.service; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import creator.loki.feature.level.domain.UserStats; import creator.loki.feature.level.repository.UserStatsRepository; public class MessageCooldownService { diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java index b77e48e..1d5f905 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java @@ -7,11 +7,11 @@ import creator.loki.command.Command; import creator.loki.command.CommandContext; import creator.loki.embed.EmbedFactory; import creator.loki.feature.config.repository.ConfigRepository; -import creator.loki.feature.moderation.util.ModerationHelper; import creator.loki.feature.moderation.MuteRoleService; import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ModerationCaseRepository; +import creator.loki.feature.moderation.util.ModerationHelper; import creator.loki.util.MuteDurationParser; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; diff --git a/src/main/java/creator/loki/feature/radio/RadioManager.java b/src/main/java/creator/loki/feature/radio/RadioManager.java index aafbda0..1e9da08 100644 --- a/src/main/java/creator/loki/feature/radio/RadioManager.java +++ b/src/main/java/creator/loki/feature/radio/RadioManager.java @@ -34,16 +34,22 @@ public class RadioManager { this.queueRepository = queueRepository; } - public void setLavalinkManager(LavalinkManager lm) { this.lavalinkManager = lm; } + public void setLavalinkManager(LavalinkManager lm) { + this.lavalinkManager = lm; + } - public void setRadioJda(JDA radioJda) { this.radioJda = radioJda; } + public void setRadioJda(JDA radioJda) { + this.radioJda = radioJda; + } public void setRadio(long guildId, String streamUrl) { - radioStates.compute(guildId, (id, state) -> { - if (state == null) state = new GuildRadioState(); - state.streamUrl = streamUrl; - return state; - }); + radioStates.compute( + guildId, + (id, state) -> { + if (state == null) state = new GuildRadioState(); + state.streamUrl = streamUrl; + return state; + }); LoggerDecorator.music(logger, "Radio URL set for guild " + guildId + ": " + streamUrl); } @@ -65,12 +71,14 @@ public class RadioManager { if (guild == null) return; guild.getJDA().getDirectAudioController().connect(guild.getVoiceChannelById(channelId)); var self = guild.getMember(guild.getJDA().getSelfUser()); - if (!VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel(), VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + if (!VoiceHelper.waitForCondition( + () -> self.getVoiceState().inAudioChannel(), VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { LoggerDecorator.warn(logger, "Radio: voice connection timeout for guild " + guildId); return; } var link = lavalinkManager.getOrCreateLink(guildId); - if (!VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null, VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + if (!VoiceHelper.waitForCondition( + () -> link.getCachedPlayer() != null, VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { LoggerDecorator.warn(logger, "Radio: Lavalink player timeout for guild " + guildId); return; } @@ -78,43 +86,60 @@ public class RadioManager { int vol = finalState.volume; var url = finalState.streamUrl; finalState.musicManager = mm; - lavalinkManager.getOrCreateLink(guildId).loadItem(url) - .subscribe(new AbstractAudioLoadResultHandler() { - @Override - public void ontrackLoaded(TrackLoaded r) { - if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); - mm.getScheduler().enqueue(r.getTrack()); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - @Override - public void onPlaylistLoaded(dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { - if (!r.getTracks().isEmpty()) { - if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); - mm.getScheduler().enqueue(r.getTracks().get(0)); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - } - @Override - public void onSearchResultLoaded(dev.arbjerg.lavalink.client.player.SearchResult r) { - if (!r.getTracks().isEmpty()) { - if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); - mm.getScheduler().enqueue(r.getTracks().get(0)); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - } - @Override - public void noMatches() { LoggerDecorator.warn(logger, "No radio stream: " + url); } - @Override - public void loadFailed(dev.arbjerg.lavalink.client.player.LoadFailed e) { - LoggerDecorator.warn(logger, "Failed to load radio: " + (e.getException().getMessage() != null ? e.getException().getMessage() : "unknown")); - } - }); + lavalinkManager + .getOrCreateLink(guildId) + .loadItem(url) + .subscribe( + new AbstractAudioLoadResultHandler() { + @Override + public void ontrackLoaded(TrackLoaded r) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTrack()); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + + @Override + public void onPlaylistLoaded(dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { + if (!r.getTracks().isEmpty()) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } + + @Override + public void onSearchResultLoaded(dev.arbjerg.lavalink.client.player.SearchResult r) { + if (!r.getTracks().isEmpty()) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } + + @Override + public void noMatches() { + LoggerDecorator.warn(logger, "No radio stream: " + url); + } + + @Override + public void loadFailed(dev.arbjerg.lavalink.client.player.LoadFailed e) { + LoggerDecorator.warn( + logger, + "Failed to load radio: " + + (e.getException().getMessage() != null + ? e.getException().getMessage() + : "unknown")); + } + }); } public void stopRadio(long guildId) { GuildRadioState state = radioStates.remove(guildId); if (state == null) return; - if (state.musicManager != null) { state.musicManager.stop(); state.musicManager.destroy(); } + if (state.musicManager != null) { + state.musicManager.stop(); + state.musicManager.destroy(); + } if (lavalinkManager != null) { Link link = lavalinkManager.getClient().getLinkIfCached(guildId); if (link != null) link.destroy().subscribe(); @@ -131,9 +156,13 @@ public class RadioManager { return state != null && state.streamUrl != null; } - public boolean hasRadioBot() { return radioJda != null; } + public boolean hasRadioBot() { + return radioJda != null; + } - public JDA getRadioJda() { return radioJda; } + public JDA getRadioJda() { + return radioJda; + } public int getVolume(long guildId) { GuildRadioState state = radioStates.get(guildId); @@ -147,7 +176,8 @@ public class RadioManager { if (lavalinkManager != null) { var link = lavalinkManager.getClient().getLinkIfCached(guildId); if (link != null && link.getCachedPlayer() != null) { - link.createOrUpdatePlayer().setVolume(state.volume) + link.createOrUpdatePlayer() + .setVolume(state.volume) .subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())); } }