refactor: BotModule SRP, BlackjackService, FeatureTables, DatabaseMigration

- BotModule 149→63: @Provides вынесены в DatabaseModule
- BlackjackService 172→152, FeatureTables 193→147, DatabaseMigration 193→149
- ShutdownManager (118) уже ≤150
This commit is contained in:
loki5512344 2026-07-04 19:19:13 +02:00
parent 0d916fbd96
commit f6b037921b
11 changed files with 435 additions and 503 deletions

View file

@ -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<Map<String, Object>>() {}.getType();
Map<String, Object> 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<String, Object> parseJson(String path) {
try (var reader = new InputStreamReader(new FileInputStream(path), UTF_8)) {
var type = new TypeToken<Map<String, Object>>() {}.getType();
return (Map<String, Object>) gson.fromJson(reader, type);
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка миграции данных", e);
return null;
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
var userStats = (Map<String, Object>) 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<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) 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<String, Object>) 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<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
var reputation = (Map<String, Object>) 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<String, Object> 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<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
var shipPairs = (Map<String, Object>) 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<String, Object> 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<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
var customRoles = (Map<String, Object>) 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<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
for (Map.Entry<String, Object> 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<String, Object>) 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();
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<String> playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
List<String> 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<String> 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<String> 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<String> playerCards,
List<String> 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,

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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