fix: repair broken test compilation, flaky BlackjackServiceTest, and split large files <=150 lines
- RankCardGeneratorTest: mock record ProfileData via direct constructor instead of non-existent getters - SmokeCommandRegistrationTest: drop removed feature.roast refs, update registrar signatures - BlackjackServiceTest.deductsBetFromCash: atLeast(1) + first captured account (flaky on blackjack) - Split >150-line files across music, moderation, config, logging, reactroles, ticket, database, economy, voice, help, radio, tiktok, level domains (KISS/DRY/SOLID, files <=150 lines)
This commit is contained in:
parent
c5bac60ebf
commit
1483c5cee0
116 changed files with 4369 additions and 3768 deletions
|
|
@ -7,7 +7,10 @@ public class BotConfig {
|
|||
String prefix, String activity, int healthHttpPort, String superAdminId, String dbPath) {}
|
||||
|
||||
public record AiContext(
|
||||
String deepseekApiKey, String aiChannelId, int aiContextMsgs, int aiMsgLength,
|
||||
String deepseekApiKey,
|
||||
String aiChannelId,
|
||||
int aiContextMsgs,
|
||||
int aiMsgLength,
|
||||
int aiContextChars) {}
|
||||
|
||||
public record AiDossier(int aiDossierMsgs, int aiDossierUpdateHours, String aiDebateChannelId) {}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ public final class ConfigLoader {
|
|||
return new BotConfig(
|
||||
new BotConfig.Tokens(mainToken, musicToken, radioToken),
|
||||
new BotConfig.BotSettings(prefix, activity, healthHttpPort, superAdminId, dbPath),
|
||||
new BotConfig.AiContext(deepseekApiKey, aiChannelId, aiContextMsgs, aiMsgLength, aiContextChars),
|
||||
new BotConfig.AiContext(
|
||||
deepseekApiKey, aiChannelId, aiContextMsgs, aiMsgLength, aiContextChars),
|
||||
new BotConfig.AiDossier(aiDossierMsgs, aiDossierUpdateHours, aiDebateChannelId),
|
||||
lavalinkConfig,
|
||||
externalUrl);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ import org.slf4j.LoggerFactory;
|
|||
public class DatabaseMigration {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
||||
private final Database database;
|
||||
private final JsonMigration jsonMigration;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public DatabaseMigration(Database database) {
|
||||
this.database = database;
|
||||
this.jsonMigration = new JsonMigration(database, logger);
|
||||
}
|
||||
|
||||
public void migrateFromJson(final String jsonPath) {
|
||||
|
|
@ -32,24 +34,33 @@ public class DatabaseMigration {
|
|||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (alreadyMigrated()) {
|
||||
return;
|
||||
}
|
||||
jsonMigration.userStats(data);
|
||||
jsonMigration.reputation(data);
|
||||
jsonMigration.shipPairs(data);
|
||||
jsonMigration.customRoles(data);
|
||||
markMigrated();
|
||||
LoggerDecorator.success(logger, "Миграция завершена успешно");
|
||||
}
|
||||
|
||||
private boolean alreadyMigrated() {
|
||||
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;
|
||||
}
|
||||
return rs.next() && "true".equals(rs.getString("value"));
|
||||
} catch (SQLException ignored) {
|
||||
return false;
|
||||
}
|
||||
migrateUserStats(data);
|
||||
migrateReputation(data);
|
||||
migrateShipPairs(data);
|
||||
migrateCustomRoles(data);
|
||||
}
|
||||
|
||||
private void markMigrated() {
|
||||
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, "Миграция завершена успешно");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
@ -62,108 +73,4 @@ public class DatabaseMigration {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateUserStats(final Map<String, Object> data) {
|
||||
var userStats = (Map<String, Object>) data.get("userStats");
|
||||
if (userStats == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция userStats: " + userStats.size() + " записей");
|
||||
var sql =
|
||||
"INSERT OR REPLACE INTO user_stats"
|
||||
+ " (user_id, level, total_xp, message_count,"
|
||||
+ " voice_minutes, last_message_time, last_voice_join)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
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());
|
||||
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
|
||||
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 мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции userStats", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateReputation(final Map<String, Object> data) {
|
||||
var reputation = (Map<String, Object>) data.get("reputation");
|
||||
if (reputation == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция reputation: " + reputation.size() + " записей");
|
||||
try (var a =
|
||||
database.prepare("INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)")) {
|
||||
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) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции reputation", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateShipPairs(final Map<String, Object> data) {
|
||||
var shipPairs = (Map<String, Object>) data.get("shipPairs");
|
||||
if (shipPairs == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция shipPairs: " + shipPairs.size() + " записей");
|
||||
try (var b =
|
||||
database.prepare(
|
||||
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)")) {
|
||||
for (var entry : shipPairs.entrySet()) {
|
||||
var users = entry.getKey().split("-");
|
||||
if (users.length == 2) {
|
||||
b.setString(1, users[0]);
|
||||
b.setString(2, users[1]);
|
||||
b.setDouble(3, ((Number) entry.getValue()).doubleValue());
|
||||
b.executeUpdate();
|
||||
}
|
||||
}
|
||||
LoggerDecorator.success(logger, "shipPairs мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции shipPairs", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateCustomRoles(final Map<String, Object> data) {
|
||||
var customRoles = (Map<String, Object>) data.get("custom_roles");
|
||||
if (customRoles == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция custom_roles");
|
||||
try (var c =
|
||||
database.prepare(
|
||||
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)")) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LoggerDecorator.success(logger, "custom_roles мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции custom_roles", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
121
src/main/java/creator/loki/database/JsonMigration.java
Normal file
121
src/main/java/creator/loki/database/JsonMigration.java
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package creator.loki.database;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
final class JsonMigration {
|
||||
private final Database database;
|
||||
private final Logger logger;
|
||||
|
||||
JsonMigration(Database database, Logger logger) {
|
||||
this.database = database;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void userStats(final Map<String, Object> data) {
|
||||
var userStats = (Map<String, Object>) data.get("userStats");
|
||||
if (userStats == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция userStats: " + userStats.size() + " записей");
|
||||
var sql =
|
||||
"INSERT OR REPLACE INTO user_stats"
|
||||
+ " (user_id, level, total_xp, message_count,"
|
||||
+ " voice_minutes, last_message_time, last_voice_join)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
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());
|
||||
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
|
||||
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 мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции userStats", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void reputation(final Map<String, Object> data) {
|
||||
var reputation = (Map<String, Object>) data.get("reputation");
|
||||
if (reputation == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция reputation: " + reputation.size() + " записей");
|
||||
try (var a =
|
||||
database.prepare("INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)")) {
|
||||
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) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции reputation", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void shipPairs(final Map<String, Object> data) {
|
||||
var shipPairs = (Map<String, Object>) data.get("shipPairs");
|
||||
if (shipPairs == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция shipPairs: " + shipPairs.size() + " записей");
|
||||
try (var b =
|
||||
database.prepare(
|
||||
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)")) {
|
||||
for (var entry : shipPairs.entrySet()) {
|
||||
var users = entry.getKey().split("-");
|
||||
if (users.length == 2) {
|
||||
b.setString(1, users[0]);
|
||||
b.setString(2, users[1]);
|
||||
b.setDouble(3, ((Number) entry.getValue()).doubleValue());
|
||||
b.executeUpdate();
|
||||
}
|
||||
}
|
||||
LoggerDecorator.success(logger, "shipPairs мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции shipPairs", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void customRoles(final Map<String, Object> data) {
|
||||
var customRoles = (Map<String, Object>) data.get("custom_roles");
|
||||
if (customRoles == null) {
|
||||
return;
|
||||
}
|
||||
LoggerDecorator.database(logger, "Миграция custom_roles");
|
||||
try (var c =
|
||||
database.prepare(
|
||||
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)")) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LoggerDecorator.success(logger, "custom_roles мигрированы");
|
||||
} catch (SQLException e) {
|
||||
LoggerDecorator.error(logger, "Ошибка миграции custom_roles", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package creator.loki.database.schema;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
||||
final class CounterTables {
|
||||
private CounterTables() {}
|
||||
|
||||
static void create(final 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
|
||||
)""");
|
||||
}
|
||||
|
||||
static void createIndexes(final Database database) {
|
||||
database.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package creator.loki.database.schema;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
||||
final class EconomyTables {
|
||||
private EconomyTables() {}
|
||||
|
||||
static void create(final 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
|
||||
)""");
|
||||
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)
|
||||
)""");
|
||||
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)
|
||||
)""");
|
||||
}
|
||||
|
||||
static void createIndexes(final 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_economy_balance ON economy((cash + bank) DESC)");
|
||||
}
|
||||
}
|
||||
|
|
@ -5,170 +5,19 @@ import creator.loki.database.Database;
|
|||
public final class FeatureTables {
|
||||
private 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 REACTION_ROLES =
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS reaction_roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
role_id TEXT NOT NULL,
|
||||
UNIQUE(message_id, emoji)
|
||||
)""";
|
||||
private static final String TICKETS =
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
channel_id TEXT PRIMARY KEY,
|
||||
guild_id TEXT NOT NULL,
|
||||
creator_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
closed INTEGER DEFAULT 0,
|
||||
claimed_by TEXT
|
||||
)""";
|
||||
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(final Database 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(TICKETS);
|
||||
database.execute(RATINGS);
|
||||
database.execute(REACTION_ROLES);
|
||||
ModerationTables.create(database);
|
||||
InteractionTables.create(database);
|
||||
EconomyTables.create(database);
|
||||
VoiceTables.create(database);
|
||||
CounterTables.create(database);
|
||||
createIndexes(database);
|
||||
}
|
||||
|
||||
private static void createIndexes(final Database database) {
|
||||
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);
|
||||
}
|
||||
ModerationTables.createIndexes(database);
|
||||
EconomyTables.createIndexes(database);
|
||||
VoiceTables.createIndexes(database);
|
||||
CounterTables.createIndexes(database);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package creator.loki.database.schema;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
||||
final class InteractionTables {
|
||||
private InteractionTables() {}
|
||||
|
||||
static void create(final Database database) {
|
||||
database.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS reaction_roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
role_id TEXT NOT NULL,
|
||||
UNIQUE(message_id, emoji)
|
||||
)""");
|
||||
database.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
channel_id TEXT PRIMARY KEY,
|
||||
guild_id TEXT NOT NULL,
|
||||
creator_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
closed INTEGER DEFAULT 0,
|
||||
claimed_by TEXT
|
||||
)""");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package creator.loki.database.schema;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
||||
final class ModerationTables {
|
||||
private ModerationTables() {}
|
||||
|
||||
static void create(final 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
|
||||
)""");
|
||||
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
|
||||
)""");
|
||||
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)
|
||||
)""");
|
||||
}
|
||||
|
||||
static void createIndexes(final Database database) {
|
||||
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)");
|
||||
}
|
||||
}
|
||||
51
src/main/java/creator/loki/database/schema/VoiceTables.java
Normal file
51
src/main/java/creator/loki/database/schema/VoiceTables.java
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package creator.loki.database.schema;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
||||
final class VoiceTables {
|
||||
private VoiceTables() {}
|
||||
|
||||
static void create(final 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)
|
||||
)""");
|
||||
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
|
||||
)""");
|
||||
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
|
||||
)""");
|
||||
}
|
||||
|
||||
static void createIndexes(final Database database) {
|
||||
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)");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package creator.loki.feature.config;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
final class CounterChannelUpdater {
|
||||
private final ConfigRepository configRepo;
|
||||
|
||||
CounterChannelUpdater(ConfigRepository configRepo) {
|
||||
this.configRepo = configRepo;
|
||||
}
|
||||
|
||||
void update(final Guild guild) {
|
||||
if (guild == null) {
|
||||
return;
|
||||
}
|
||||
String channelId = configRepo.get("counter.channel_id");
|
||||
String format = configRepo.get("counter.format");
|
||||
if (channelId != null && format != null) {
|
||||
var channel = guild.getVoiceChannelById(channelId);
|
||||
if (channel != null) {
|
||||
int count = guild.getMemberCount();
|
||||
channel.getManager().setName(format.replace("{count}", String.valueOf(count))).queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package creator.loki.feature.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
final class LevelRoleConfig {
|
||||
private static final String PREFIX = "level_role_";
|
||||
|
||||
private LevelRoleConfig() {}
|
||||
|
||||
static Map<Integer, String> byLevel(final Map<String, String> all) {
|
||||
Map<Integer, String> result = new TreeMap<>();
|
||||
for (var entry : all.entrySet()) {
|
||||
if (entry.getKey().startsWith(PREFIX)) {
|
||||
result.put(Integer.parseInt(entry.getKey().substring(PREFIX.length())), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,10 @@ package creator.loki.feature.config;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.components.label.Label;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInput;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
import net.dv8tion.jda.api.modals.Modal;
|
||||
|
||||
class SetupDashboardBuilder {
|
||||
private static final Color BLURPLE = new Color(88, 101, 242);
|
||||
|
|
@ -90,91 +81,7 @@ class SetupDashboardBuilder {
|
|||
}
|
||||
|
||||
private void addLevelRolesSection(final EmbedBuilder embed) {
|
||||
Map<String, String> allConfig = configRepo.getAll();
|
||||
long levelRoleCount =
|
||||
allConfig.keySet().stream().filter(k -> k.startsWith("level_role_")).count();
|
||||
embed.addField("🏆 Роли за уровни", "Настроено ролей: **" + levelRoleCount + "**", false);
|
||||
}
|
||||
|
||||
void openCounterFormatModal(final ButtonInteractionEvent event) {
|
||||
String currentFormat = configRepo.get("counter.format");
|
||||
if (currentFormat == null) {
|
||||
currentFormat = "【👥】Участники: {count}";
|
||||
}
|
||||
|
||||
TextInput formatInput =
|
||||
TextInput.create("format", TextInputStyle.SHORT)
|
||||
.setValue(currentFormat)
|
||||
.setPlaceholder("【👥】Участники: {count}")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("setup_counter_format_modal", "✏️ Формат счётчика")
|
||||
.addComponents(Label.of("Формат строки", formatInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
}
|
||||
|
||||
void openLevelRoleAddModal(final ButtonInteractionEvent event) {
|
||||
TextInput levelInput =
|
||||
TextInput.create("level", TextInputStyle.SHORT)
|
||||
.setPlaceholder("10")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
TextInput roleInput =
|
||||
TextInput.create("role", TextInputStyle.SHORT)
|
||||
.setPlaceholder("Название роли")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("setup_levelrole_add_modal", "➕ Добавить роль за уровень")
|
||||
.addComponents(Label.of("Уровень", levelInput), Label.of("Название роли", roleInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
}
|
||||
|
||||
void openLevelRoleRemoveSelect(final ButtonInteractionEvent event) {
|
||||
Map<String, String> allConfig = configRepo.getAll();
|
||||
var options =
|
||||
allConfig.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith("level_role_"))
|
||||
.map(
|
||||
e -> {
|
||||
String level = e.getKey().substring("level_role_".length());
|
||||
return "Уровень " + level + " — " + e.getValue();
|
||||
})
|
||||
.toList();
|
||||
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringSelectMenu.Builder menuBuilder =
|
||||
StringSelectMenu.create("setup_levelrole_remove_select")
|
||||
.setPlaceholder("Выбери роль для удаления");
|
||||
|
||||
var levelKeys =
|
||||
allConfig.keySet().stream()
|
||||
.filter(k -> k.startsWith("level_role_"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (String key : levelKeys) {
|
||||
String level = key.substring("level_role_".length());
|
||||
String roleName = allConfig.get(key);
|
||||
String label = "Уровень " + level + " — " + roleName;
|
||||
menuBuilder.addOption(label, level);
|
||||
}
|
||||
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalComponents(
|
||||
ActionRow.of(menuBuilder.build()),
|
||||
ActionRow.of(Button.secondary("setup_back", "🔙 Назад")))
|
||||
.queue();
|
||||
Map<Integer, String> levelRoles = LevelRoleConfig.byLevel(configRepo.getAll());
|
||||
embed.addField("🏆 Роли за уровни", "Настроено ролей: **" + levelRoles.size() + "**", false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package creator.loki.feature.config;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
|
||||
final class SetupInteractions {
|
||||
private final ConfigRepository configRepo;
|
||||
private final SetupPanelBuilder panelBuilder;
|
||||
private final CounterChannelUpdater counterUpdater;
|
||||
|
||||
SetupInteractions(ConfigRepository configRepo, SetupPanelBuilder panelBuilder) {
|
||||
this.configRepo = configRepo;
|
||||
this.panelBuilder = panelBuilder;
|
||||
this.counterUpdater = new CounterChannelUpdater(configRepo);
|
||||
}
|
||||
|
||||
void onEntitySelect(final EntitySelectInteractionEvent event) {
|
||||
String value = event.getValues().get(0).getId();
|
||||
switch (event.getComponentId()) {
|
||||
case "setup_voice_trigger" -> {
|
||||
configRepo.set("voice.trigger_channel_id", value);
|
||||
panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_voice_category" -> {
|
||||
configRepo.set("voice.category_id", value);
|
||||
panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_counter_channel" -> {
|
||||
configRepo.set("counter.channel_id", value);
|
||||
counterUpdater.update(event.getGuild());
|
||||
panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_logs_moderation" -> {
|
||||
configRepo.set("moderation.log_channel_id", value);
|
||||
panelBuilder.showLoggingPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_logs_levelup" -> {
|
||||
configRepo.set("levelup.notification_channel_id", value);
|
||||
panelBuilder.showLoggingPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
void onStringSelect(final StringSelectInteractionEvent event) {
|
||||
if ("setup_levelrole_remove_select".equals(event.getComponentId())) {
|
||||
String selected = event.getValues().get(0);
|
||||
configRepo.delete("level_role_" + selected);
|
||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
}
|
||||
|
||||
void onModal(final ModalInteractionEvent event) {
|
||||
switch (event.getModalId()) {
|
||||
case "setup_counter_format_modal" -> {
|
||||
String format = event.getValue("format").getAsString();
|
||||
configRepo.set("counter.format", format);
|
||||
counterUpdater.update(event.getGuild());
|
||||
panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_levelrole_add_modal" -> {
|
||||
String levelStr = event.getValue("level").getAsString();
|
||||
String role = event.getValue("role").getAsString();
|
||||
configRepo.set("level_role_" + levelStr, role);
|
||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/main/java/creator/loki/feature/config/SetupModals.java
Normal file
85
src/main/java/creator/loki/feature/config/SetupModals.java
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package creator.loki.feature.config;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.components.label.Label;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInput;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
import net.dv8tion.jda.api.modals.Modal;
|
||||
|
||||
final class SetupModals {
|
||||
private final ConfigRepository configRepo;
|
||||
|
||||
SetupModals(ConfigRepository configRepo) {
|
||||
this.configRepo = configRepo;
|
||||
}
|
||||
|
||||
void openCounterFormatModal(final ButtonInteractionEvent event) {
|
||||
String currentFormat = configRepo.get("counter.format");
|
||||
if (currentFormat == null) {
|
||||
currentFormat = "【👥】Участники: {count}";
|
||||
}
|
||||
|
||||
TextInput formatInput =
|
||||
TextInput.create("format", TextInputStyle.SHORT)
|
||||
.setValue(currentFormat)
|
||||
.setPlaceholder("【👥】Участники: {count}")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("setup_counter_format_modal", "✏️ Формат счётчика")
|
||||
.addComponents(Label.of("Формат строки", formatInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
}
|
||||
|
||||
void openLevelRoleAddModal(final ButtonInteractionEvent event) {
|
||||
TextInput levelInput =
|
||||
TextInput.create("level", TextInputStyle.SHORT)
|
||||
.setPlaceholder("10")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
TextInput roleInput =
|
||||
TextInput.create("role", TextInputStyle.SHORT)
|
||||
.setPlaceholder("Название роли")
|
||||
.setRequired(true)
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("setup_levelrole_add_modal", "➕ Добавить роль за уровень")
|
||||
.addComponents(Label.of("Уровень", levelInput), Label.of("Название роли", roleInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
}
|
||||
|
||||
void openLevelRoleRemoveSelect(final ButtonInteractionEvent event) {
|
||||
var levelRoles = LevelRoleConfig.byLevel(configRepo.getAll());
|
||||
|
||||
if (levelRoles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringSelectMenu.Builder menuBuilder =
|
||||
StringSelectMenu.create("setup_levelrole_remove_select")
|
||||
.setPlaceholder("Выбери роль для удаления");
|
||||
|
||||
for (var entry : levelRoles.entrySet()) {
|
||||
menuBuilder.addOption(
|
||||
"Уровень " + entry.getKey() + " — " + entry.getValue(), entry.getKey().toString());
|
||||
}
|
||||
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalComponents(
|
||||
ActionRow.of(menuBuilder.build()),
|
||||
ActionRow.of(Button.secondary("setup_back", "🔙 Назад")))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package creator.loki.feature.config;
|
|||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
|
|
@ -115,14 +114,7 @@ class SetupPanelBuilder {
|
|||
}
|
||||
|
||||
void showLevelRolesPanel(Guild guild, final InteractionHook hook) {
|
||||
Map<String, String> allConfig = configRepo.getAll();
|
||||
var levelRoles =
|
||||
allConfig.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith("level_role_"))
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
e -> Integer.parseInt(e.getKey().substring("level_role_".length())),
|
||||
Map.Entry::getValue));
|
||||
Map<Integer, String> levelRoles = LevelRoleConfig.byLevel(configRepo.getAll());
|
||||
|
||||
EmbedBuilder embed = new EmbedBuilder().setTitle("🏆 Роли за уровни").setColor(BLURPLE);
|
||||
|
||||
|
|
@ -130,15 +122,9 @@ class SetupPanelBuilder {
|
|||
embed.setDescription("❌ Роли не настроены");
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
levelRoles.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.forEach(
|
||||
e ->
|
||||
sb.append("**Уровень ")
|
||||
.append(e.getKey())
|
||||
.append(":** `")
|
||||
.append(e.getValue())
|
||||
.append("`\n"));
|
||||
levelRoles.forEach(
|
||||
(level, role) ->
|
||||
sb.append("**Уровень ").append(level).append(":** `").append(role).append("`\n"));
|
||||
embed.setDescription(sb.toString());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import com.google.inject.Singleton;
|
|||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
||||
|
|
@ -20,6 +20,8 @@ public class SetupSlashHandler extends ListenerAdapter {
|
|||
private final ConfigRepository configRepo;
|
||||
private final SetupPanelBuilder panelBuilder;
|
||||
private final SetupDashboardBuilder dashBuilder;
|
||||
private final SetupModals modals;
|
||||
private final SetupInteractions interactions;
|
||||
private volatile boolean registered = false;
|
||||
|
||||
@Inject
|
||||
|
|
@ -27,6 +29,8 @@ public class SetupSlashHandler extends ListenerAdapter {
|
|||
this.configRepo = configRepo;
|
||||
this.panelBuilder = new SetupPanelBuilder(configRepo);
|
||||
this.dashBuilder = new SetupDashboardBuilder(configRepo);
|
||||
this.modals = new SetupModals(configRepo);
|
||||
this.interactions = new SetupInteractions(configRepo, panelBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -63,89 +67,41 @@ public class SetupSlashHandler extends ListenerAdapter {
|
|||
case "setup_voice" -> panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||
case "setup_counter" -> panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||
case "setup_logs" -> panelBuilder.showLoggingPanel(event.getGuild(), event.getHook());
|
||||
case "setup_levelroles" -> panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
case "setup_levelroles" ->
|
||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
case "setup_back" -> showDashboard(event.getGuild(), event.getHook());
|
||||
case "setup_counter_format" -> dashBuilder.openCounterFormatModal(event);
|
||||
case "setup_levelrole_add" -> dashBuilder.openLevelRoleAddModal(event);
|
||||
case "setup_levelrole_remove" -> dashBuilder.openLevelRoleRemoveSelect(event);
|
||||
case "setup_counter_format" -> modals.openCounterFormatModal(event);
|
||||
case "setup_levelrole_add" -> modals.openLevelRoleAddModal(event);
|
||||
case "setup_levelrole_remove" -> modals.openLevelRoleRemoveSelect(event);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntitySelectInteraction(@NotNull final EntitySelectInteractionEvent event) {
|
||||
String id = event.getComponentId();
|
||||
if (!id.startsWith("setup_")) {
|
||||
if (!event.getComponentId().startsWith("setup_")) {
|
||||
return;
|
||||
}
|
||||
event.deferEdit().queue();
|
||||
|
||||
String value = event.getValues().get(0).getId();
|
||||
|
||||
switch (id) {
|
||||
case "setup_voice_trigger" -> {
|
||||
configRepo.set("voice.trigger_channel_id", value);
|
||||
panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_voice_category" -> {
|
||||
configRepo.set("voice.category_id", value);
|
||||
panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_counter_channel" -> {
|
||||
configRepo.set("counter.channel_id", value);
|
||||
updateCounterChannelName(event.getGuild());
|
||||
panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_logs_moderation" -> {
|
||||
configRepo.set("moderation.log_channel_id", value);
|
||||
panelBuilder.showLoggingPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_logs_levelup" -> {
|
||||
configRepo.set("levelup.notification_channel_id", value);
|
||||
panelBuilder.showLoggingPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
interactions.onEntitySelect(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStringSelectInteraction(@NotNull final StringSelectInteractionEvent event) {
|
||||
String id = event.getComponentId();
|
||||
if (!id.startsWith("setup_")) {
|
||||
if (!event.getComponentId().startsWith("setup_")) {
|
||||
return;
|
||||
}
|
||||
event.deferEdit().queue();
|
||||
|
||||
if ("setup_levelrole_remove_select".equals(id)) {
|
||||
String selected = event.getValues().get(0);
|
||||
configRepo.delete("level_role_" + selected);
|
||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
interactions.onStringSelect(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModalInteraction(@NotNull final ModalInteractionEvent event) {
|
||||
String id = event.getModalId();
|
||||
if (!id.startsWith("setup_")) {
|
||||
if (!event.getModalId().startsWith("setup_")) {
|
||||
return;
|
||||
}
|
||||
event.deferEdit().queue();
|
||||
|
||||
switch (id) {
|
||||
case "setup_counter_format_modal" -> {
|
||||
String format = event.getValue("format").getAsString();
|
||||
configRepo.set("counter.format", format);
|
||||
updateCounterChannelName(event.getGuild());
|
||||
panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
case "setup_levelrole_add_modal" -> {
|
||||
String levelStr = event.getValue("level").getAsString();
|
||||
String role = event.getValue("role").getAsString();
|
||||
configRepo.set("level_role_" + levelStr, role);
|
||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
interactions.onModal(event);
|
||||
}
|
||||
|
||||
private void showDashboard(Guild guild, final InteractionHook hook) {
|
||||
|
|
@ -153,19 +109,4 @@ public class SetupSlashHandler extends ListenerAdapter {
|
|||
.setComponents(panelBuilder.buildDashboardButtons())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void updateCounterChannelName(Guild guild) {
|
||||
if (guild == null) {
|
||||
return;
|
||||
}
|
||||
String channelId = configRepo.get("counter.channel_id");
|
||||
String format = configRepo.get("counter.format");
|
||||
if (channelId != null && format != null) {
|
||||
var channel = guild.getVoiceChannelById(channelId);
|
||||
if (channel != null) {
|
||||
int count = guild.getMemberCount();
|
||||
channel.getManager().setName(format.replace("{count}", String.valueOf(count))).queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ public class BankCommand implements Command {
|
|||
if (balance < amount) {
|
||||
sendError(
|
||||
ctx,
|
||||
"Недостаточно " + (isDep ? "наличных" : "в банке")
|
||||
+ "! У вас **" + balance + "** LC");
|
||||
"Недостаточно " + (isDep ? "наличных" : "в банке") + "! У вас **" + balance + "** LC");
|
||||
return;
|
||||
}
|
||||
if (isDep) {
|
||||
|
|
@ -102,8 +101,11 @@ public class BankCommand implements Command {
|
|||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
isDep ? "Депозит" : "Снятие",
|
||||
"Ты " + (isDep ? "положил" : "снял")
|
||||
+ " **" + amount + "** LC "
|
||||
"Ты "
|
||||
+ (isDep ? "положил" : "снял")
|
||||
+ " **"
|
||||
+ amount
|
||||
+ "** LC "
|
||||
+ (isDep ? "в банк!" : "из банка!")))
|
||||
.queue();
|
||||
}
|
||||
|
|
@ -123,8 +125,6 @@ public class BankCommand implements Command {
|
|||
}
|
||||
|
||||
private void sendError(final CommandContext ctx, final String msg) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg))
|
||||
.queue();
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package creator.loki.feature.economy.command.job;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.economy.service.game.JobService;
|
||||
|
||||
|
|
@ -78,7 +75,7 @@ public class JobCommand implements Command {
|
|||
return;
|
||||
}
|
||||
|
||||
JobService.JobInfo info = findJob(current);
|
||||
JobService.JobInfo info = JobEmbeds.findJob(jobService.listJobs(), current);
|
||||
if (info == null) {
|
||||
context
|
||||
.getChannel()
|
||||
|
|
@ -88,59 +85,15 @@ public class JobCommand implements Command {
|
|||
return;
|
||||
}
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("💼 Твоя работа")
|
||||
.withColor(new Color(100, 200, 255))
|
||||
.addField("Должность", info.emoji() + " " + info.displayName(), false)
|
||||
.addField("Зарплата", info.minPayout() + " – " + info.maxPayout() + " LC", true)
|
||||
.addField("Требуемый уровень", String.valueOf(info.requiredLevel()), true)
|
||||
.build())
|
||||
.queue();
|
||||
context.getChannel().sendMessageEmbeds(JobEmbeds.current(info)).queue();
|
||||
}
|
||||
|
||||
private void handleList(final CommandContext context) {
|
||||
var jobs = jobService.listJobs();
|
||||
var desc = new StringBuilder();
|
||||
|
||||
for (var job : jobs) {
|
||||
desc.append(job.emoji())
|
||||
.append(" **")
|
||||
.append(job.displayName())
|
||||
.append("** — ")
|
||||
.append(job.minPayout())
|
||||
.append("–")
|
||||
.append(job.maxPayout())
|
||||
.append(" LC (ур. ")
|
||||
.append(job.requiredLevel())
|
||||
.append(")\n");
|
||||
}
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("📋 Доступные работы")
|
||||
.withDescription(desc.toString())
|
||||
.withColor(new Color(100, 200, 255))
|
||||
.withFooter("!job join <название> — устроиться на работу")
|
||||
.build())
|
||||
.queue();
|
||||
context.getChannel().sendMessageEmbeds(JobEmbeds.list(jobService.listJobs())).queue();
|
||||
}
|
||||
|
||||
private void handleJoin(final CommandContext context, final String userId, final String name) {
|
||||
var jobs = jobService.listJobs();
|
||||
JobService.JobInfo target = null;
|
||||
|
||||
for (var job : jobs) {
|
||||
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||
target = job;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
JobService.JobInfo target = JobEmbeds.findJob(jobService.listJobs(), name);
|
||||
if (target == null) {
|
||||
context
|
||||
.getChannel()
|
||||
|
|
@ -166,13 +119,4 @@ public class JobCommand implements Command {
|
|||
+ "Используй `!work` чтобы начать работать."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private JobService.JobInfo findJob(final String name) {
|
||||
for (var job : jobService.listJobs()) {
|
||||
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||
return job;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package creator.loki.feature.economy.command.job;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.economy.service.game.JobService;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
/** Построение embed-сообщений для команды работы. */
|
||||
public final class JobEmbeds {
|
||||
private static final Color COLOR = new Color(100, 200, 255);
|
||||
|
||||
private JobEmbeds() {}
|
||||
|
||||
public static MessageEmbed current(final JobService.JobInfo info) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("💼 Твоя работа")
|
||||
.withColor(COLOR)
|
||||
.addField("Должность", info.emoji() + " " + info.displayName(), false)
|
||||
.addField("Зарплата", info.minPayout() + " – " + info.maxPayout() + " LC", true)
|
||||
.addField("Требуемый уровень", String.valueOf(info.requiredLevel()), true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MessageEmbed list(final List<JobService.JobInfo> jobs) {
|
||||
var desc = new StringBuilder();
|
||||
for (var job : jobs) {
|
||||
desc.append(job.emoji())
|
||||
.append(" **")
|
||||
.append(job.displayName())
|
||||
.append("** — ")
|
||||
.append(job.minPayout())
|
||||
.append("–")
|
||||
.append(job.maxPayout())
|
||||
.append(" LC (ур. ")
|
||||
.append(job.requiredLevel())
|
||||
.append(")\n");
|
||||
}
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📋 Доступные работы")
|
||||
.withDescription(desc.toString())
|
||||
.withColor(COLOR)
|
||||
.withFooter("!job join <название> — устроиться на работу")
|
||||
.build();
|
||||
}
|
||||
|
||||
public static JobService.JobInfo findJob(final List<JobService.JobInfo> jobs, final String name) {
|
||||
for (var job : jobs) {
|
||||
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
||||
return job;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
package creator.loki.feature.economy.command.minigame;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.economy.service.EconomyService;
|
||||
import creator.loki.feature.economy.service.game.BlackjackService;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
public class BlackjackCommand implements Command {
|
||||
|
||||
|
|
@ -111,7 +108,7 @@ public class BlackjackCommand implements Command {
|
|||
if (!state.isOver()) {
|
||||
activeGames.add(userId);
|
||||
}
|
||||
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
||||
context.getChannel().sendMessageEmbeds(BlackjackEmbeds.game(state)).queue();
|
||||
}
|
||||
|
||||
private void handleAction(
|
||||
|
|
@ -129,50 +126,6 @@ public class BlackjackCommand implements Command {
|
|||
if (state.isOver()) {
|
||||
activeGames.remove(userId);
|
||||
}
|
||||
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
||||
}
|
||||
|
||||
private MessageEmbed buildGameEmbed(final BlackjackService.BlackjackState state) {
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🃏 Блэкджек")
|
||||
.withColor(new Color(34, 139, 34))
|
||||
.addField("Твои карты", formatCards(state.playerCards(), state.playerSum()), true);
|
||||
if (state.isOver()) {
|
||||
embed
|
||||
.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
|
||||
.addField("Результат", formatResult(state), false);
|
||||
} else {
|
||||
embed
|
||||
.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
|
||||
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
|
||||
}
|
||||
return embed.addField("Ставка", state.bet() + " LC", false).build();
|
||||
}
|
||||
|
||||
private String formatCards(final String[] cards, final int sum) {
|
||||
return String.join(" + ", cards) + " = " + sum;
|
||||
}
|
||||
|
||||
private String formatHiddenDealerCards(final String[] cards) {
|
||||
var sb = new StringBuilder("?");
|
||||
for (int i = 1; i < cards.length; i++) {
|
||||
sb.append(" + ").append(cards[i]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatResult(final BlackjackService.BlackjackState state) {
|
||||
long payout = state.payout();
|
||||
if (payout > state.bet()) {
|
||||
return "🎉 Блэкджек! +" + payout + " LC";
|
||||
}
|
||||
if (payout > 0) {
|
||||
return "✅ Ты выиграл +" + payout + " LC";
|
||||
}
|
||||
if (payout == state.bet()) {
|
||||
return "🤝 Ничья. Возврат " + payout + " LC";
|
||||
}
|
||||
return "💔 Ты проиграл " + state.bet() + " LC";
|
||||
context.getChannel().sendMessageEmbeds(BlackjackEmbeds.game(state)).queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package creator.loki.feature.economy.command.minigame;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.economy.service.game.BlackjackService;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
/** Построение embed-сообщений для игры в блэкджек. */
|
||||
public final class BlackjackEmbeds {
|
||||
private BlackjackEmbeds() {}
|
||||
|
||||
public static MessageEmbed game(final BlackjackService.BlackjackState state) {
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🃏 Блэкджек")
|
||||
.withColor(new Color(34, 139, 34))
|
||||
.addField("Твои карты", formatCards(state.playerCards(), state.playerSum()), true);
|
||||
if (state.isOver()) {
|
||||
embed
|
||||
.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
|
||||
.addField("Результат", formatResult(state), false);
|
||||
} else {
|
||||
embed
|
||||
.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
|
||||
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
|
||||
}
|
||||
return embed.addField("Ставка", state.bet() + " LC", false).build();
|
||||
}
|
||||
|
||||
private static String formatCards(final String[] cards, final int sum) {
|
||||
return String.join(" + ", cards) + " = " + sum;
|
||||
}
|
||||
|
||||
private static String formatHiddenDealerCards(final String[] cards) {
|
||||
var sb = new StringBuilder("?");
|
||||
for (int i = 1; i < cards.length; i++) {
|
||||
sb.append(" + ").append(cards[i]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String formatResult(final BlackjackService.BlackjackState state) {
|
||||
long payout = state.payout();
|
||||
if (payout > state.bet()) {
|
||||
return "🎉 Блэкджек! +" + payout + " LC";
|
||||
}
|
||||
if (payout > 0) {
|
||||
return "✅ Ты выиграл +" + payout + " LC";
|
||||
}
|
||||
if (payout == state.bet()) {
|
||||
return "🤝 Ничья. Возврат " + payout + " LC";
|
||||
}
|
||||
return "💔 Ты проиграл " + state.bet() + " LC";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package creator.loki.feature.economy.repository;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
||||
|
||||
/** Маппинг строки economy-таблицы в {@link EconomyAccount}. */
|
||||
public final class EconomyRowMapper {
|
||||
private EconomyRowMapper() {}
|
||||
|
||||
public static EconomyAccount map(final ResultSet rs) throws SQLException {
|
||||
return new EconomyAccountBuilder()
|
||||
.userId(rs.getString("user_id"))
|
||||
.cash(rs.getLong("cash"))
|
||||
.bank(rs.getLong("bank"))
|
||||
.totalEarned(rs.getLong("total_earned"))
|
||||
.totalSpent(rs.getLong("total_spent"))
|
||||
.lastDaily(rs.getString("last_daily"))
|
||||
.dailyStreak(rs.getInt("daily_streak"))
|
||||
.fishingLuck(rs.getInt("fishing_luck"))
|
||||
.efficiency(rs.getInt("efficiency"))
|
||||
.lastFish(rs.getString("last_fish"))
|
||||
.job(rs.getString("job"))
|
||||
.lastWork(rs.getString("last_work"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package creator.loki.feature.economy.repository;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
|
||||
/** SQL-запросы и низкоуровневые помощники для economy-таблицы. */
|
||||
public final class EconomySql {
|
||||
public static final int MAX_TOP = 100;
|
||||
|
||||
public static final String COLUMNS =
|
||||
"user_id, cash, bank, total_earned, total_spent,"
|
||||
+ " last_daily, daily_streak, fishing_luck,"
|
||||
+ " efficiency, last_fish, job, last_work";
|
||||
|
||||
private EconomySql() {}
|
||||
|
||||
public static String selectSql() {
|
||||
return "SELECT " + COLUMNS + " FROM economy WHERE user_id = ?";
|
||||
}
|
||||
|
||||
public static String upsertSql() {
|
||||
return "INSERT INTO economy ("
|
||||
+ COLUMNS
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(user_id) DO UPDATE SET "
|
||||
+ "cash = excluded.cash, bank = excluded.bank, total_earned = excluded.total_earned, "
|
||||
+ "total_spent = excluded.total_spent, last_daily = excluded.last_daily, "
|
||||
+ "daily_streak = excluded.daily_streak, fishing_luck = excluded.fishing_luck, "
|
||||
+ "efficiency = excluded.efficiency, last_fish = excluded.last_fish, "
|
||||
+ "job = excluded.job, last_work = excluded.last_work";
|
||||
}
|
||||
|
||||
public static String topByBalanceSql() {
|
||||
return "SELECT "
|
||||
+ COLUMNS
|
||||
+ " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
|
||||
}
|
||||
|
||||
public static void setNullableString(
|
||||
final PreparedStatement stmt, final int index, final String value) throws SQLException {
|
||||
if (value != null) {
|
||||
stmt.setString(index, value);
|
||||
} else {
|
||||
stmt.setNull(index, Types.VARCHAR);
|
||||
}
|
||||
}
|
||||
|
||||
public static int clampLimit(final int limit) {
|
||||
if (limit <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(limit, MAX_TOP);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package creator.loki.feature.economy.repository;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -11,57 +8,25 @@ import java.util.Optional;
|
|||
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SqlEconomyRepository implements EconomyRepository {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
|
||||
private static final int MAX_TOP = 100;
|
||||
private final Database database;
|
||||
|
||||
private static final String COLUMNS =
|
||||
"user_id, cash, bank, total_earned, total_spent,"
|
||||
+ " last_daily, daily_streak, fishing_luck,"
|
||||
+ " efficiency, last_fish, job, last_work";
|
||||
private final Database database;
|
||||
|
||||
public SqlEconomyRepository(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
private static String selectSql() {
|
||||
return "SELECT " + COLUMNS + " FROM economy WHERE user_id = ?";
|
||||
}
|
||||
|
||||
private static String upsertSql() {
|
||||
return "INSERT INTO economy ("
|
||||
+ COLUMNS
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(user_id) DO UPDATE SET "
|
||||
+ "cash = excluded.cash, bank = excluded.bank, total_earned = excluded.total_earned, "
|
||||
+ "total_spent = excluded.total_spent, last_daily = excluded.last_daily, "
|
||||
+ "daily_streak = excluded.daily_streak, fishing_luck = excluded.fishing_luck, "
|
||||
+ "efficiency = excluded.efficiency, last_fish = excluded.last_fish, "
|
||||
+ "job = excluded.job, last_work = excluded.last_work";
|
||||
}
|
||||
|
||||
private static void setNullableString(PreparedStatement stmt, final int index, final String value)
|
||||
throws SQLException {
|
||||
if (value != null) {
|
||||
stmt.setString(index, value);
|
||||
} else {
|
||||
stmt.setNull(index, Types.VARCHAR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<EconomyAccount> findById(final String userId) {
|
||||
var sql = selectSql();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
try (var stmt = database.prepare(EconomySql.selectSql())) {
|
||||
stmt.setString(1, userId);
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(mapResultSetToAccount(rs));
|
||||
return Optional.of(EconomyRowMapper.map(rs));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
|
|
@ -72,20 +37,19 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public void save(final EconomyAccount account) {
|
||||
var sql = upsertSql();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
try (var stmt = database.prepare(EconomySql.upsertSql())) {
|
||||
stmt.setString(1, account.getUserId());
|
||||
stmt.setLong(2, account.getCash());
|
||||
stmt.setLong(3, account.getBank());
|
||||
stmt.setLong(4, account.getTotalEarned());
|
||||
stmt.setLong(5, account.getTotalSpent());
|
||||
setNullableString(stmt, 6, account.getLastDaily());
|
||||
EconomySql.setNullableString(stmt, 6, account.getLastDaily());
|
||||
stmt.setInt(7, account.getDailyStreak());
|
||||
stmt.setInt(8, account.getFishingLuck());
|
||||
stmt.setInt(9, account.getEfficiency());
|
||||
setNullableString(stmt, 10, account.getLastFish());
|
||||
setNullableString(stmt, 11, account.getJob());
|
||||
setNullableString(stmt, 12, account.getLastWork());
|
||||
EconomySql.setNullableString(stmt, 10, account.getLastFish());
|
||||
EconomySql.setNullableString(stmt, 11, account.getJob());
|
||||
EconomySql.setNullableString(stmt, 12, account.getLastWork());
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
logger.error("Error saving economy account for {}", account.getUserId(), e);
|
||||
|
|
@ -94,8 +58,7 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public boolean exists(final String userId) {
|
||||
var sql = "SELECT 1 FROM economy WHERE user_id = ?";
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
try (var stmt = database.prepare("SELECT 1 FROM economy WHERE user_id = ?")) {
|
||||
stmt.setString(1, userId);
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
return rs.next();
|
||||
|
|
@ -108,17 +71,13 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
|
||||
@Override
|
||||
public List<EconomyAccount> findTopByBalance(final int limit) {
|
||||
var lim = clampLimit(limit);
|
||||
var sql =
|
||||
"SELECT "
|
||||
+ COLUMNS
|
||||
+ " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
|
||||
var lim = EconomySql.clampLimit(limit);
|
||||
var list = new ArrayList<EconomyAccount>();
|
||||
try (var stmt = database.prepare(sql)) {
|
||||
try (var stmt = database.prepare(EconomySql.topByBalanceSql())) {
|
||||
stmt.setInt(1, lim);
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
list.add(mapResultSetToAccount(rs));
|
||||
list.add(EconomyRowMapper.map(rs));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
|
|
@ -126,28 +85,4 @@ public class SqlEconomyRepository implements EconomyRepository {
|
|||
}
|
||||
return Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
private static int clampLimit(final int limit) {
|
||||
if (limit <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(limit, MAX_TOP);
|
||||
}
|
||||
|
||||
private EconomyAccount mapResultSetToAccount(final ResultSet rs) throws SQLException {
|
||||
return new EconomyAccountBuilder()
|
||||
.userId(rs.getString("user_id"))
|
||||
.cash(rs.getLong("cash"))
|
||||
.bank(rs.getLong("bank"))
|
||||
.totalEarned(rs.getLong("total_earned"))
|
||||
.totalSpent(rs.getLong("total_spent"))
|
||||
.lastDaily(rs.getString("last_daily"))
|
||||
.dailyStreak(rs.getInt("daily_streak"))
|
||||
.fishingLuck(rs.getInt("fishing_luck"))
|
||||
.efficiency(rs.getInt("efficiency"))
|
||||
.lastFish(rs.getString("last_fish"))
|
||||
.job(rs.getString("job"))
|
||||
.lastWork(rs.getString("last_work"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package creator.loki.feature.economy.service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/** Игровое поле рыбалки: сетка, целевая рыба и количество совпадений. */
|
||||
public class FishingBoard {
|
||||
private static final String[] FISH = {
|
||||
"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"
|
||||
};
|
||||
private static final String WAVE = "\uD83C\uDF0A";
|
||||
|
||||
private final String[] rows;
|
||||
private final String targetFish;
|
||||
private final int correctCount;
|
||||
|
||||
public FishingBoard(int size, int fishTypes) {
|
||||
String[] available = Arrays.copyOf(FISH, fishTypes);
|
||||
String[][] grid = new String[size][size];
|
||||
int[] counts = new int[fishTypes];
|
||||
var rng = ThreadLocalRandom.current();
|
||||
|
||||
for (int r = 0; r < size; r++) {
|
||||
for (int c = 0; c < size; c++) {
|
||||
if (rng.nextInt(100) < 40) {
|
||||
grid[r][c] = WAVE;
|
||||
} else {
|
||||
int fi = rng.nextInt(fishTypes);
|
||||
grid[r][c] = available[fi];
|
||||
counts[fi]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int targetIdx;
|
||||
do {
|
||||
targetIdx = rng.nextInt(fishTypes);
|
||||
} while (counts[targetIdx] == 0);
|
||||
|
||||
String[] rowLines = new String[size];
|
||||
for (int r = 0; r < size; r++) {
|
||||
rowLines[r] = String.join(" ", grid[r]);
|
||||
}
|
||||
this.rows = rowLines;
|
||||
this.targetFish = available[targetIdx];
|
||||
this.correctCount = counts[targetIdx];
|
||||
}
|
||||
|
||||
public String[] getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public String getTargetFish() {
|
||||
return targetFish;
|
||||
}
|
||||
|
||||
public int getCorrectCount() {
|
||||
return correctCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package creator.loki.feature.economy.service;
|
||||
|
||||
/** Правила сложности рыбалки. */
|
||||
public enum FishingDifficulty {
|
||||
EASY(5, 2, 15, 30000),
|
||||
NORMAL(7, 3, 30, 45000),
|
||||
HARD(9, 4, 50, 60000);
|
||||
|
||||
private final int size;
|
||||
private final int fishTypes;
|
||||
private final int baseReward;
|
||||
private final long timeLimitMs;
|
||||
|
||||
FishingDifficulty(int size, int fishTypes, int baseReward, long timeLimitMs) {
|
||||
this.size = size;
|
||||
this.fishTypes = fishTypes;
|
||||
this.baseReward = baseReward;
|
||||
this.timeLimitMs = timeLimitMs;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getFishTypes() {
|
||||
return fishTypes;
|
||||
}
|
||||
|
||||
public long getTimeLimitMs() {
|
||||
return timeLimitMs;
|
||||
}
|
||||
|
||||
public long computeReward(final long elapsed, final int luck) {
|
||||
double ratio = (double) elapsed / timeLimitMs;
|
||||
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
|
||||
return (long) (baseReward * speedMult * (1 + luck * 0.01));
|
||||
}
|
||||
|
||||
public static FishingDifficulty from(final String difficulty) {
|
||||
switch (difficulty.toLowerCase()) {
|
||||
case "easy":
|
||||
return EASY;
|
||||
case "normal":
|
||||
return NORMAL;
|
||||
case "hard":
|
||||
return HARD;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,13 @@
|
|||
package creator.loki.feature.economy.service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FishingService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FishingService.class);
|
||||
|
||||
private static final String[] FISH = {
|
||||
"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"
|
||||
};
|
||||
private static final String WAVE = "\uD83C\uDF0A";
|
||||
private static final long COOLDOWN_MS = 7200000;
|
||||
|
||||
private final EconomyRepository repository;
|
||||
|
|
@ -39,65 +29,23 @@ public class FishingService {
|
|||
}
|
||||
}
|
||||
|
||||
int size;
|
||||
int fishTypes;
|
||||
int baseReward;
|
||||
switch (difficulty.toLowerCase()) {
|
||||
case "easy" -> {
|
||||
size = 5;
|
||||
fishTypes = 2;
|
||||
baseReward = 15;
|
||||
}
|
||||
case "normal" -> {
|
||||
size = 7;
|
||||
fishTypes = 3;
|
||||
baseReward = 30;
|
||||
}
|
||||
case "hard" -> {
|
||||
size = 9;
|
||||
fishTypes = 4;
|
||||
baseReward = 50;
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String[] available = Arrays.copyOf(FISH, fishTypes);
|
||||
String[][] grid = new String[size][size];
|
||||
int[] counts = new int[fishTypes];
|
||||
var rng = ThreadLocalRandom.current();
|
||||
|
||||
for (int r = 0; r < size; r++) {
|
||||
for (int c = 0; c < size; c++) {
|
||||
if (rng.nextInt(100) < 40) {
|
||||
grid[r][c] = WAVE;
|
||||
} else {
|
||||
int fi = rng.nextInt(fishTypes);
|
||||
grid[r][c] = available[fi];
|
||||
counts[fi]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int targetIdx;
|
||||
do {
|
||||
targetIdx = rng.nextInt(fishTypes);
|
||||
} while (counts[targetIdx] == 0);
|
||||
|
||||
String targetFish = available[targetIdx];
|
||||
int correctCount = counts[targetIdx];
|
||||
|
||||
String[] rows = new String[size];
|
||||
for (int r = 0; r < size; r++) {
|
||||
rows[r] = String.join(" ", grid[r]);
|
||||
FishingDifficulty diff = FishingDifficulty.from(difficulty);
|
||||
if (diff == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FishingBoard board = new FishingBoard(diff.getSize(), diff.getFishTypes());
|
||||
ActiveFish session =
|
||||
new ActiveFish(targetFish, correctCount, System.currentTimeMillis(), difficulty, rows);
|
||||
new ActiveFish(
|
||||
board.getTargetFish(),
|
||||
board.getCorrectCount(),
|
||||
System.currentTimeMillis(),
|
||||
difficulty,
|
||||
board.getRows());
|
||||
activeFishing.put(userId, session);
|
||||
|
||||
return new FishResult(rows, targetFish, correctCount, 0, account.getCash());
|
||||
return new FishResult(
|
||||
board.getRows(), board.getTargetFish(), board.getCorrectCount(), 0, account.getCash());
|
||||
}
|
||||
|
||||
public FishResult checkAnswer(final String userId, int answer) {
|
||||
|
|
@ -118,34 +66,17 @@ public class FishingService {
|
|||
return null;
|
||||
}
|
||||
|
||||
FishingDifficulty diff = FishingDifficulty.from(difficulty);
|
||||
if (diff == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EconomyAccount account = repository.getAccount(userId);
|
||||
long elapsed = System.currentTimeMillis() - session.startTime();
|
||||
|
||||
int baseReward;
|
||||
long timeLimit;
|
||||
switch (difficulty.toLowerCase()) {
|
||||
case "easy" -> {
|
||||
baseReward = 15;
|
||||
timeLimit = 30000;
|
||||
}
|
||||
case "normal" -> {
|
||||
baseReward = 30;
|
||||
timeLimit = 45000;
|
||||
}
|
||||
case "hard" -> {
|
||||
baseReward = 50;
|
||||
timeLimit = 60000;
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
long reward = 0;
|
||||
if (answer == session.correctCount() && elapsed <= timeLimit) {
|
||||
double ratio = (double) elapsed / timeLimit;
|
||||
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
|
||||
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
|
||||
if (answer == session.correctCount() && elapsed <= diff.getTimeLimitMs()) {
|
||||
reward = diff.computeReward(elapsed, account.getFishingLuck());
|
||||
}
|
||||
|
||||
long newCash = account.getCash() + reward;
|
||||
|
|
|
|||
|
|
@ -54,9 +54,7 @@ public class GlazbobaCommand implements Command {
|
|||
Thread.sleep(1000);
|
||||
loadingMsg.editMessage("🔐 Взламываю Discord...").queue();
|
||||
Thread.sleep(1000);
|
||||
loadingMsg
|
||||
.editMessageEmbeds(buildProfileEmbed(target, context).build())
|
||||
.queue();
|
||||
loadingMsg.editMessageEmbeds(buildProfileEmbed(target, context).build()).queue();
|
||||
} catch (InterruptedException e) {
|
||||
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
|
||||
}
|
||||
|
|
@ -90,9 +88,7 @@ public class GlazbobaCommand implements Command {
|
|||
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
|
||||
.addField("🎭 Роли", roles, false)
|
||||
.withColor(
|
||||
target.getColors().getPrimary() != null
|
||||
? target.getColors().getPrimary()
|
||||
: Color.BLUE)
|
||||
target.getColors().getPrimary() != null ? target.getColors().getPrimary() : Color.BLUE)
|
||||
.withFooter("Запросил: " + context.getAuthor().getUser().getName());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package creator.loki.feature.help;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
|
||||
/** Embed для списка команд выбранной категории. */
|
||||
public final class HelpCategoryEmbed {
|
||||
private HelpCategoryEmbed() {}
|
||||
|
||||
public static EmbedFactory build(
|
||||
final String category,
|
||||
final List<Command> commands,
|
||||
final StringSelectInteractionEvent event,
|
||||
final BotConfig config) {
|
||||
String emoji = HelpCategories.getEmoji(category);
|
||||
String prefix = config.getPrefix();
|
||||
|
||||
EmbedFactory embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle(emoji + " " + category)
|
||||
.withDescription(
|
||||
"Список команд категории **" + category + "**\n" + "Префикс: `" + prefix + "`")
|
||||
.withColor(HelpCategories.getColor(category));
|
||||
|
||||
StringBuilder commandList = new StringBuilder();
|
||||
int count = 0;
|
||||
for (Command cmd : commands) {
|
||||
commandList
|
||||
.append("`")
|
||||
.append(prefix)
|
||||
.append(cmd.getUsage())
|
||||
.append("`\n")
|
||||
.append(cmd.getDescription())
|
||||
.append("\n\n");
|
||||
count++;
|
||||
if (count % 8 == 0 || commandList.length() > 900) {
|
||||
embed.addField("", commandList.toString().trim(), false);
|
||||
commandList = new StringBuilder();
|
||||
}
|
||||
}
|
||||
if (commandList.length() > 0) {
|
||||
embed.addField("", commandList.toString().trim(), false);
|
||||
}
|
||||
|
||||
embed.addField(
|
||||
"💡 Подсказка", "Используй селектор ниже для переключения между категориями", false);
|
||||
|
||||
embed.withFooter(
|
||||
"Запросил: " + event.getUser().getName() + " • Всего команд: " + commands.size(),
|
||||
event.getUser().getAvatarUrl());
|
||||
|
||||
return embed;
|
||||
}
|
||||
}
|
||||
62
src/main/java/creator/loki/feature/help/HelpMainPage.java
Normal file
62
src/main/java/creator/loki/feature/help/HelpMainPage.java
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package creator.loki.feature.help;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
|
||||
/** Главная страница справки. */
|
||||
public class HelpMainPage {
|
||||
private HelpMainPage() {}
|
||||
|
||||
public static EmbedFactory build(
|
||||
final CommandContext context,
|
||||
final Map<String, List<Command>> commandsByCategory,
|
||||
final HelpMenuBuilder menuBuilder,
|
||||
final boolean isAdmin,
|
||||
final boolean isSuper,
|
||||
final boolean isNsfwChannel) {
|
||||
int totalCommands =
|
||||
menuBuilder.countVisibleCommands(commandsByCategory, isAdmin, isSuper, isNsfwChannel);
|
||||
String categoriesText =
|
||||
menuBuilder.buildCategoriesText(commandsByCategory, isAdmin, isSuper, isNsfwChannel);
|
||||
String prefix = menuBuilder.getPrefix();
|
||||
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📚 Справка по командам Loki Bot")
|
||||
.withDescription(
|
||||
"Привет! Я многофункциональный Discord бот с системой уровней, "
|
||||
+ "личными голосовыми каналами и множеством развлекательных команд.\n\n"
|
||||
+ "**Префикс команд:** `"
|
||||
+ prefix
|
||||
+ "`\n"
|
||||
+ "**Всего команд:** "
|
||||
+ totalCommands
|
||||
+ "\n\n"
|
||||
+ "**📂 Категории команд:**\n"
|
||||
+ categoriesText
|
||||
+ "\n"
|
||||
+ "Используй селектор ниже для просмотра команд категории.")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.withThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
|
||||
.addField(
|
||||
"💡 Как использовать",
|
||||
"Все команды - с префиксом `"
|
||||
+ prefix
|
||||
+ "`\n"
|
||||
+ "Примеры: `"
|
||||
+ prefix
|
||||
+ "rank`, `"
|
||||
+ prefix
|
||||
+ "ship @user1 @user2`, `"
|
||||
+ prefix
|
||||
+ "voice-setup info`",
|
||||
false)
|
||||
.withFooter(
|
||||
"Запросил: " + context.getAuthor().getUser().getName(),
|
||||
context.getAuthor().getUser().getAvatarUrl());
|
||||
}
|
||||
}
|
||||
91
src/main/java/creator/loki/feature/help/HelpMenuBuilder.java
Normal file
91
src/main/java/creator/loki/feature/help/HelpMenuBuilder.java
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package creator.loki.feature.help;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.CommandRegistry;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.entities.emoji.Emoji;
|
||||
|
||||
/** Построение селектора категорий и группировка команд для справки. */
|
||||
public class HelpMenuBuilder {
|
||||
private final CommandRegistry registry;
|
||||
private final BotConfig config;
|
||||
|
||||
public HelpMenuBuilder(CommandRegistry registry, BotConfig config) {
|
||||
this.registry = registry;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public Map<String, List<Command>> groupByCategory() {
|
||||
return registry.getAllCommands().stream().collect(Collectors.groupingBy(Command::getCategory));
|
||||
}
|
||||
|
||||
public StringSelectMenu buildSelector(
|
||||
final Map<String, List<Command>> commandsByCategory,
|
||||
final boolean isAdmin,
|
||||
final boolean isSuper,
|
||||
final boolean isNsfwChannel) {
|
||||
StringSelectMenu.Builder menuBuilder =
|
||||
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
|
||||
commandsByCategory.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(
|
||||
category -> {
|
||||
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
String emoji = HelpCategories.getEmoji(category);
|
||||
String description = HelpCategories.getDescription(category);
|
||||
int count = commandsByCategory.get(category).size();
|
||||
menuBuilder.addOption(
|
||||
category,
|
||||
"category:" + category,
|
||||
description + " (" + count + " команд)",
|
||||
Emoji.fromUnicode(emoji));
|
||||
});
|
||||
return menuBuilder.build();
|
||||
}
|
||||
|
||||
public int countVisibleCommands(
|
||||
final Map<String, List<Command>> commandsByCategory,
|
||||
final boolean isAdmin,
|
||||
final boolean isSuper,
|
||||
final boolean isNsfwChannel) {
|
||||
return commandsByCategory.entrySet().stream()
|
||||
.filter(e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
||||
.mapToInt(e -> e.getValue().size())
|
||||
.sum();
|
||||
}
|
||||
|
||||
public String buildCategoriesText(
|
||||
final Map<String, List<Command>> commandsByCategory,
|
||||
final boolean isAdmin,
|
||||
final boolean isSuper,
|
||||
final boolean isNsfwChannel) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
commandsByCategory.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(
|
||||
category -> {
|
||||
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
text.append(HelpCategories.getEmoji(category))
|
||||
.append(" **")
|
||||
.append(category)
|
||||
.append("** - ")
|
||||
.append(commandsByCategory.get(category).size())
|
||||
.append(" команд\n");
|
||||
});
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return config.getPrefix();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,26 @@
|
|||
package creator.loki.feature.help;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.CommandRegistry;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
|
||||
@Singleton
|
||||
public class HelpMenuHandler extends ListenerAdapter {
|
||||
private final CommandRegistry registry;
|
||||
private final HelpMenuBuilder menuBuilder;
|
||||
private final BotConfig config;
|
||||
|
||||
@Inject
|
||||
public HelpMenuHandler(CommandRegistry registry, BotConfig config) {
|
||||
this.registry = registry;
|
||||
this.menuBuilder = new HelpMenuBuilder(registry, config);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
|
@ -43,116 +37,44 @@ public class HelpMenuHandler extends ListenerAdapter {
|
|||
|
||||
String category = selectedValue.substring("category:".length());
|
||||
|
||||
// Проверка прав для категории "Настройка"
|
||||
if ("Настройка".equals(category)
|
||||
&& event.getMember() != null
|
||||
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
|
||||
event.reply("❌ Эта категория доступна только администраторам!").setEphemeral(true).queue();
|
||||
if (!canViewCategory(event, category)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("Супер-Админ".equals(category)
|
||||
&& !config.getSuperAdminId().equals(event.getUser().getId())) {
|
||||
event.reply("❌ Эта категория доступна только владельцу бота.").setEphemeral(true).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
var commands = registry.getAllCommands();
|
||||
Map<String, List<Command>> commandsByCategory =
|
||||
commands.stream().collect(Collectors.groupingBy(Command::getCategory));
|
||||
|
||||
var commandsByCategory = menuBuilder.groupByCategory();
|
||||
List<Command> categoryCommands = commandsByCategory.get(category);
|
||||
if (categoryCommands == null || categoryCommands.isEmpty()) {
|
||||
event.reply("❌ В этой категории нет команд!").setEphemeral(true).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаём embed для категории
|
||||
EmbedFactory embed = createCategoryEmbed(category, categoryCommands, event);
|
||||
|
||||
// Пересоздаём селектор
|
||||
StringSelectMenu.Builder menuBuilder =
|
||||
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
|
||||
|
||||
boolean isAdmin =
|
||||
event.getMember() != null && event.getMember().hasPermission(Permission.ADMINISTRATOR);
|
||||
boolean isSuper = config.getSuperAdminId().equals(event.getUser().getId());
|
||||
boolean isNsfwChannel = isNsfw(event);
|
||||
|
||||
commandsByCategory.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(
|
||||
cat -> {
|
||||
if (!HelpCategories.isCategoryVisible(cat, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String emoji = HelpCategories.getEmoji(cat);
|
||||
int count = commandsByCategory.get(cat).size();
|
||||
|
||||
menuBuilder.addOption(
|
||||
cat,
|
||||
"category:" + cat,
|
||||
count + " команд",
|
||||
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
|
||||
});
|
||||
|
||||
event.editMessageEmbeds(embed.build()).setComponents(ActionRow.of(menuBuilder.build())).queue();
|
||||
event
|
||||
.editMessageEmbeds(
|
||||
HelpCategoryEmbed.build(category, categoryCommands, event, config).build())
|
||||
.setComponents(
|
||||
ActionRow.of(
|
||||
menuBuilder.buildSelector(commandsByCategory, isAdmin, isSuper, isNsfwChannel)))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private EmbedFactory createCategoryEmbed(
|
||||
final String category,
|
||||
final List<Command> commands,
|
||||
final StringSelectInteractionEvent event) {
|
||||
String emoji = HelpCategories.getEmoji(category);
|
||||
Color color = HelpCategories.getColor(category);
|
||||
|
||||
EmbedFactory embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle(emoji + " " + category)
|
||||
.withDescription(
|
||||
"Список команд категории **"
|
||||
+ category
|
||||
+ "**\n"
|
||||
+ "Префикс: `"
|
||||
+ config.getPrefix()
|
||||
+ "`")
|
||||
.withColor(color);
|
||||
|
||||
// Группируем команды по 8 в поле
|
||||
StringBuilder commandList = new StringBuilder();
|
||||
int count = 0;
|
||||
|
||||
for (Command cmd : commands) {
|
||||
commandList
|
||||
.append("`")
|
||||
.append(config.getPrefix())
|
||||
.append(cmd.getUsage())
|
||||
.append("`\n")
|
||||
.append(cmd.getDescription())
|
||||
.append("\n\n");
|
||||
count++;
|
||||
|
||||
// Если достигли 8 команд или лимита символов, создаём новое поле
|
||||
if (count % 8 == 0 || commandList.length() > 900) {
|
||||
embed.addField("", commandList.toString().trim(), false);
|
||||
commandList = new StringBuilder();
|
||||
}
|
||||
private boolean canViewCategory(final StringSelectInteractionEvent event, final String category) {
|
||||
if ("Настройка".equals(category)
|
||||
&& event.getMember() != null
|
||||
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
|
||||
event.reply("❌ Эта категория доступна только администраторам!").setEphemeral(true).queue();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Добавляем оставшиеся команды
|
||||
if (commandList.length() > 0) {
|
||||
embed.addField("", commandList.toString().trim(), false);
|
||||
if ("Супер-Админ".equals(category)
|
||||
&& !config.getSuperAdminId().equals(event.getUser().getId())) {
|
||||
event.reply("❌ Эта категория доступна только владельцу бота.").setEphemeral(true).queue();
|
||||
return false;
|
||||
}
|
||||
|
||||
embed.addField(
|
||||
"💡 Подсказка", "Используй селектор ниже для переключения между категориями", false);
|
||||
|
||||
embed.withFooter(
|
||||
"Запросил: " + event.getUser().getName() + " • Всего команд: " + commands.size(),
|
||||
event.getUser().getAvatarUrl());
|
||||
|
||||
return embed;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isNsfw(final StringSelectInteractionEvent event) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,23 @@
|
|||
package creator.loki.feature.help.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.CommandRegistry;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.HelpCategories;
|
||||
import creator.loki.feature.help.HelpMainPage;
|
||||
import creator.loki.feature.help.HelpMenuBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
||||
|
||||
public class HelpCommand implements Command {
|
||||
private final CommandRegistry registry;
|
||||
private final HelpMenuBuilder menuBuilder;
|
||||
private final BotConfig config;
|
||||
|
||||
public HelpCommand(CommandRegistry registry, BotConfig config) {
|
||||
this.registry = registry;
|
||||
this.menuBuilder = new HelpMenuBuilder(registry, config);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
|
@ -39,9 +33,7 @@ public class HelpCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var commands = registry.getAllCommands();
|
||||
Map<String, List<Command>> commandsByCategory =
|
||||
commands.stream().collect(Collectors.groupingBy(Command::getCategory));
|
||||
var commandsByCategory = menuBuilder.groupByCategory();
|
||||
|
||||
User viewer =
|
||||
context.getAuthor() != null
|
||||
|
|
@ -52,37 +44,15 @@ public class HelpCommand implements Command {
|
|||
boolean isSuper = config.getSuperAdminId().equals(viewer.getId());
|
||||
boolean isNsfwChannel = isNsfw(context);
|
||||
|
||||
// Главная страница
|
||||
EmbedFactory embed =
|
||||
createMainPage(context, commandsByCategory, isAdmin, isSuper, isNsfwChannel);
|
||||
|
||||
// Создаём селектор категорий
|
||||
StringSelectMenu.Builder menuBuilder =
|
||||
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
|
||||
|
||||
commandsByCategory.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(
|
||||
category -> {
|
||||
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String emoji = HelpCategories.getEmoji(category);
|
||||
String description = HelpCategories.getDescription(category);
|
||||
int count = commandsByCategory.get(category).size();
|
||||
|
||||
menuBuilder.addOption(
|
||||
category,
|
||||
"category:" + category,
|
||||
description + " (" + count + " команд)",
|
||||
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
|
||||
});
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(embed.build())
|
||||
.addComponents(ActionRow.of(menuBuilder.build()))
|
||||
.sendMessageEmbeds(
|
||||
HelpMainPage.build(
|
||||
context, commandsByCategory, menuBuilder, isAdmin, isSuper, isNsfwChannel)
|
||||
.build())
|
||||
.addComponents(
|
||||
ActionRow.of(
|
||||
menuBuilder.buildSelector(commandsByCategory, isAdmin, isSuper, isNsfwChannel)))
|
||||
.queue();
|
||||
}
|
||||
|
||||
|
|
@ -90,74 +60,6 @@ public class HelpCommand implements Command {
|
|||
return context.getChannel() instanceof IAgeRestrictedChannel arc && arc.isNSFW();
|
||||
}
|
||||
|
||||
private EmbedFactory createMainPage(
|
||||
final CommandContext context,
|
||||
final Map<String, List<Command>> commandsByCategory,
|
||||
final boolean isAdmin,
|
||||
final boolean isSuper,
|
||||
final boolean isNsfwChannel) {
|
||||
int totalCommands =
|
||||
commandsByCategory.entrySet().stream()
|
||||
.filter(
|
||||
e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
||||
.mapToInt(e -> e.getValue().size())
|
||||
.sum();
|
||||
|
||||
StringBuilder categoriesText = new StringBuilder();
|
||||
commandsByCategory.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(
|
||||
category -> {
|
||||
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String emoji = HelpCategories.getEmoji(category);
|
||||
int count = commandsByCategory.get(category).size();
|
||||
categoriesText
|
||||
.append(emoji)
|
||||
.append(" **")
|
||||
.append(category)
|
||||
.append("** - ")
|
||||
.append(count)
|
||||
.append(" команд\n");
|
||||
});
|
||||
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📚 Справка по командам Loki Bot")
|
||||
.withDescription(
|
||||
"Привет! Я многофункциональный Discord бот с системой уровней, "
|
||||
+ "личными голосовыми каналами и множеством развлекательных команд.\n\n"
|
||||
+ "**Префикс команд:** `"
|
||||
+ config.getPrefix()
|
||||
+ "`\n"
|
||||
+ "**Всего команд:** "
|
||||
+ totalCommands
|
||||
+ "\n\n"
|
||||
+ "**📂 Категории команд:**\n"
|
||||
+ categoriesText.toString()
|
||||
+ "\n"
|
||||
+ "Используй селектор ниже для просмотра команд категории.")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.withThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
|
||||
.addField(
|
||||
"💡 Как использовать",
|
||||
"Все команды - с префиксом `"
|
||||
+ config.getPrefix()
|
||||
+ "`\n"
|
||||
+ "Примеры: `"
|
||||
+ config.getPrefix()
|
||||
+ "rank`, `"
|
||||
+ config.getPrefix()
|
||||
+ "ship @user1 @user2`, `"
|
||||
+ config.getPrefix()
|
||||
+ "voice-setup info`",
|
||||
false)
|
||||
.withFooter(
|
||||
"Запросил: " + context.getAuthor().getUser().getName(),
|
||||
context.getAuthor().getUser().getAvatarUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.UTILITY;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package creator.loki.feature.info;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
public final class ServerStatsEmbedBuilder {
|
||||
private ServerStatsEmbedBuilder() {}
|
||||
|
||||
public record MemberStats(long total, long bots, long humans, long online) {}
|
||||
|
||||
public record ChannelCounts(
|
||||
long textChannels, long voiceChannels, long categories, long totalChannels) {}
|
||||
|
||||
public record BoostInfo(int boostCount, int boostTier) {}
|
||||
|
||||
public record GuildMeta(int roles, int emojis, String created, String ownerName) {}
|
||||
|
||||
public static MemberStats countMembers(final List<Member> members) {
|
||||
long total = members.size();
|
||||
long bots = members.stream().filter(m -> m.getUser().isBot()).count();
|
||||
long humans = total - bots;
|
||||
long online =
|
||||
members.stream()
|
||||
.filter(
|
||||
m ->
|
||||
m.getOnlineStatus() != null
|
||||
&& m.getOnlineStatus() != OnlineStatus.OFFLINE
|
||||
&& m.getOnlineStatus() != OnlineStatus.UNKNOWN)
|
||||
.count();
|
||||
return new MemberStats(total, bots, humans, online);
|
||||
}
|
||||
|
||||
public static MessageEmbed build(
|
||||
final Guild guild,
|
||||
final MemberStats memberStats,
|
||||
final ChannelCounts channels,
|
||||
final BoostInfo boost,
|
||||
final GuildMeta meta) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📊 Статистика сервера")
|
||||
.withThumbnail(guild.getIconUrl())
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField(
|
||||
"👤 Участники",
|
||||
"Всего: **"
|
||||
+ memberStats.total()
|
||||
+ "**\n👥 Людей: **"
|
||||
+ memberStats.humans()
|
||||
+ "**\n🤖 Ботов: **"
|
||||
+ memberStats.bots()
|
||||
+ "**\n🟢 Онлайн: **"
|
||||
+ memberStats.online()
|
||||
+ "**",
|
||||
true)
|
||||
.addField(
|
||||
"💬 Каналы",
|
||||
"Текстовых: **"
|
||||
+ channels.textChannels()
|
||||
+ "**\n🔊 Голосовых: **"
|
||||
+ channels.voiceChannels()
|
||||
+ "**\n📁 Категорий: **"
|
||||
+ channels.categories()
|
||||
+ "**\nВсего: **"
|
||||
+ channels.totalChannels()
|
||||
+ "**",
|
||||
true)
|
||||
.addField(
|
||||
"⚙️ Прочее",
|
||||
"Ролей: **"
|
||||
+ meta.roles()
|
||||
+ "**\n😀 Эмодзи: **"
|
||||
+ meta.emojis()
|
||||
+ "**\n🚀 Бустов: **"
|
||||
+ boost.boostCount()
|
||||
+ "** (ур. "
|
||||
+ boost.boostTier()
|
||||
+ ")",
|
||||
true)
|
||||
.withFooter("Создан: " + meta.created() + " | Владелец: " + meta.ownerName())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
package creator.loki.feature.info.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.info.ServerStatsEmbedBuilder;
|
||||
import creator.loki.feature.info.ServerStatsEmbedBuilder.BoostInfo;
|
||||
import creator.loki.feature.info.ServerStatsEmbedBuilder.ChannelCounts;
|
||||
import creator.loki.feature.info.ServerStatsEmbedBuilder.GuildMeta;
|
||||
import creator.loki.feature.info.ServerStatsEmbedBuilder.MemberStats;
|
||||
import creator.loki.feature.serverstats.ServerStatsService;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.utils.FileUpload;
|
||||
|
||||
public class ServerStatsCommand implements Command {
|
||||
|
|
@ -47,7 +52,6 @@ public class ServerStatsCommand implements Command {
|
|||
}
|
||||
|
||||
String arg = context.getArgs().isEmpty() ? "" : context.getArgs().get(0).toLowerCase();
|
||||
|
||||
switch (arg) {
|
||||
case "csv" -> handleCsv(context, guild);
|
||||
case "chart" -> handleChart(context, guild);
|
||||
|
|
@ -55,93 +59,33 @@ public class ServerStatsCommand implements Command {
|
|||
}
|
||||
}
|
||||
|
||||
private record MemberStats(
|
||||
long total, long bots, long humans, long online) {}
|
||||
|
||||
private static MemberStats countMembers(
|
||||
java.util.List<net.dv8tion.jda.api.entities.Member> members) {
|
||||
long total = members.size();
|
||||
long bots = members.stream().filter(m -> m.getUser().isBot()).count();
|
||||
long humans = total - bots;
|
||||
long online =
|
||||
members.stream()
|
||||
.filter(
|
||||
m ->
|
||||
m.getOnlineStatus() != null
|
||||
&& m.getOnlineStatus()
|
||||
!= net.dv8tion.jda.api.OnlineStatus.OFFLINE
|
||||
&& m.getOnlineStatus()
|
||||
!= net.dv8tion.jda.api.OnlineStatus.UNKNOWN)
|
||||
.count();
|
||||
return new MemberStats(total, bots, humans, online);
|
||||
}
|
||||
|
||||
private void handleEmbed(final CommandContext context, final Guild guild) {
|
||||
guild.loadMembers().onSuccess(members -> sendStatsEmbed(context, guild, members));
|
||||
}
|
||||
|
||||
private record ChannelCounts(
|
||||
long textChannels, long voiceChannels, long categories, long totalChannels) {}
|
||||
|
||||
private record BoostInfo(int boostCount, int boostTier) {}
|
||||
|
||||
private record GuildMeta(int roles, int emojis, String created, String ownerName) {}
|
||||
|
||||
private void sendStatsEmbed(
|
||||
final CommandContext context,
|
||||
final Guild guild,
|
||||
java.util.List<net.dv8tion.jda.api.entities.Member> members) {
|
||||
var s = countMembers(members);
|
||||
final CommandContext context, final Guild guild, final List<Member> members) {
|
||||
MemberStats stats = ServerStatsEmbedBuilder.countMembers(members);
|
||||
long textChannels = guild.getTextChannels().size();
|
||||
long voiceChannels = guild.getVoiceChannels().size();
|
||||
long categories = guild.getCategories().size();
|
||||
long totalChannels = textChannels + voiceChannels + categories;
|
||||
int roles = guild.getRoles().size();
|
||||
int emojis = guild.getEmojis().size();
|
||||
int boostTier = guild.getBoostTier().getKey();
|
||||
int boostCount = guild.getBoostCount();
|
||||
String created =
|
||||
guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
|
||||
String created = guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
|
||||
String ownerName =
|
||||
guild.getOwner() != null
|
||||
? guild.getOwner().getUser().getGlobalName()
|
||||
: "неизвестен";
|
||||
guild.getOwner() != null ? guild.getOwner().getUser().getGlobalName() : "неизвестен";
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(buildStatsEmbed(
|
||||
guild, s,
|
||||
new ChannelCounts(textChannels, voiceChannels, categories, totalChannels),
|
||||
new BoostInfo(boostCount, boostTier),
|
||||
new GuildMeta(roles, emojis, created, ownerName)))
|
||||
.sendMessageEmbeds(
|
||||
ServerStatsEmbedBuilder.build(
|
||||
guild,
|
||||
stats,
|
||||
new ChannelCounts(textChannels, voiceChannels, categories, totalChannels),
|
||||
new BoostInfo(guild.getBoostCount(), guild.getBoostTier().getKey()),
|
||||
new GuildMeta(
|
||||
guild.getRoles().size(), guild.getEmojis().size(), created, ownerName)))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private net.dv8tion.jda.api.entities.MessageEmbed buildStatsEmbed(
|
||||
final Guild guild,
|
||||
final MemberStats memberStats,
|
||||
final ChannelCounts channels,
|
||||
final BoostInfo boost,
|
||||
final GuildMeta meta) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📊 Статистика сервера")
|
||||
.withThumbnail(guild.getIconUrl())
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField("👤 Участники",
|
||||
"Всего: **" + memberStats.total() + "**\n👥 Людей: **" + memberStats.humans()
|
||||
+ "**\n🤖 Ботов: **" + memberStats.bots() + "**\n🟢 Онлайн: **" + memberStats.online() + "**",
|
||||
true)
|
||||
.addField("💬 Каналы",
|
||||
"Текстовых: **" + channels.textChannels() + "**\n🔊 Голосовых: **" + channels.voiceChannels()
|
||||
+ "**\n📁 Категорий: **" + channels.categories() + "**\nВсего: **" + channels.totalChannels() + "**",
|
||||
true)
|
||||
.addField("⚙️ Прочее",
|
||||
"Ролей: **" + meta.roles() + "**\n😀 Эмодзи: **" + meta.emojis()
|
||||
+ "**\n🚀 Бустов: **" + boost.boostCount() + "** (ур. " + boost.boostTier() + ")",
|
||||
true)
|
||||
.withFooter("Создан: " + meta.created() + " | Владелец: " + meta.ownerName())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void handleCsv(final CommandContext context, final Guild guild) {
|
||||
context
|
||||
.getChannel()
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
package creator.loki.feature.level.commands;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.level.commands.util.LevelRoleFormatter;
|
||||
import creator.loki.feature.level.service.LevelRoleService;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
|
||||
public class LevelRoleCommand implements Command {
|
||||
private final LevelRoleService levelRoleService;
|
||||
private final ConfigRepository configRepository;
|
||||
private final LevelRoleSubcommands subcommands;
|
||||
|
||||
public LevelRoleCommand(LevelRoleService levelRoleService, ConfigRepository configRepository) {
|
||||
this.levelRoleService = levelRoleService;
|
||||
this.configRepository = configRepository;
|
||||
this.subcommands = new LevelRoleSubcommands(levelRoleService, configRepository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -56,125 +52,15 @@ public class LevelRoleCommand implements Command {
|
|||
|
||||
List<String> args = context.getArgs();
|
||||
if (args.isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
LevelRoleFormatter.buildMappingsEmbed(configRepository, levelRoleService).build())
|
||||
.queue();
|
||||
subcommands.showMappings(context);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args.get(0).toLowerCase()) {
|
||||
case "set" -> handleSet(context, args);
|
||||
case "remove" -> handleRemove(context, args);
|
||||
case "clear" -> handleClear(context);
|
||||
default ->
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
LevelRoleFormatter.buildMappingsEmbed(configRepository, levelRoleService).build())
|
||||
.queue();
|
||||
case "set" -> subcommands.set(context, args);
|
||||
case "remove" -> subcommands.remove(context, args);
|
||||
case "clear" -> subcommands.clear(context);
|
||||
default -> subcommands.showMappings(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSet(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 3) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Использование: `!levelrole set <уровень> <название роли>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(args.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Уровень должен быть числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (level < 1) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Уровень должен быть положительным числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String roleName = String.join(" ", args.subList(2, args.size()));
|
||||
if (roleName.isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Название роли не может быть пустым."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
levelRoleService.setRoleForLevel(level, roleName);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Роль назначена",
|
||||
"Роль `" + roleName + "` будет выдаваться за уровень **" + level + "**."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleRemove(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Использование: `!levelrole remove <уровень>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(args.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Уровень должен быть числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
levelRoleService.removeRoleForLevel(level);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Роль сброшена",
|
||||
"Для уровня **" + level + "** будет использоваться роль по умолчанию."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleClear(final CommandContext context) {
|
||||
Map<String, String> allConfig = configRepository.getAll();
|
||||
int removed = 0;
|
||||
for (String key : allConfig.keySet()) {
|
||||
if (key.startsWith("level_role_")) {
|
||||
configRepository.delete(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Настройки сброшены",
|
||||
"Удалено пользовательских ролей: **"
|
||||
+ removed
|
||||
+ "**. Будут использоваться роли по умолчанию."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
package creator.loki.feature.level.commands;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.level.commands.util.LevelRoleFormatter;
|
||||
import creator.loki.feature.level.service.LevelRoleService;
|
||||
|
||||
/** Обработчики подкоманд настройки ролей за уровни. */
|
||||
public class LevelRoleSubcommands {
|
||||
private final LevelRoleService levelRoleService;
|
||||
private final ConfigRepository configRepository;
|
||||
|
||||
public LevelRoleSubcommands(
|
||||
LevelRoleService levelRoleService, ConfigRepository configRepository) {
|
||||
this.levelRoleService = levelRoleService;
|
||||
this.configRepository = configRepository;
|
||||
}
|
||||
|
||||
public void showMappings(final CommandContext context) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
LevelRoleFormatter.buildMappingsEmbed(configRepository, levelRoleService).build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void set(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 3) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Использование: `!levelrole set <уровень> <название роли>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(args.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Уровень должен быть числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (level < 1) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Уровень должен быть положительным числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String roleName = String.join(" ", args.subList(2, args.size()));
|
||||
if (roleName.isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Название роли не может быть пустым."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
levelRoleService.setRoleForLevel(level, roleName);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Роль назначена",
|
||||
"Роль `" + roleName + "` будет выдаваться за уровень **" + level + "**."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void remove(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Использование: `!levelrole remove <уровень>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
int level;
|
||||
try {
|
||||
level = Integer.parseInt(args.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Уровень должен быть числом."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
levelRoleService.removeRoleForLevel(level);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Роль сброшена",
|
||||
"Для уровня **" + level + "** будет использоваться роль по умолчанию."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void clear(final CommandContext context) {
|
||||
Map<String, String> allConfig = configRepository.getAll();
|
||||
int removed = 0;
|
||||
for (String key : allConfig.keySet()) {
|
||||
if (key.startsWith("level_role_")) {
|
||||
configRepository.delete(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Настройки сброшены",
|
||||
"Удалено пользовательских ролей: **"
|
||||
+ removed
|
||||
+ "**. Будут использоваться роли по умолчанию."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,24 @@
|
|||
package creator.loki.feature.level.commands.admin;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.repository.UserStatsRepository;
|
||||
import creator.loki.feature.level.service.LevelResetService;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Одноразовая команда для сброса уровней и XP всех пользователей Сохраняет время в войсе и
|
||||
* количество сообщений
|
||||
*/
|
||||
/** Одноразовая команда для сброса уровней и XP всех пользователей. */
|
||||
public class ResetLevelsCommand implements Command {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsCommand.class);
|
||||
private static final String RESET_KEY = "levels_reset_executed";
|
||||
|
||||
private final UserStatsRepository repository;
|
||||
private final GuildConfigRepository guildConfig;
|
||||
private final LevelResetService resetService;
|
||||
|
||||
public ResetLevelsCommand(UserStatsRepository repository, GuildConfigRepository guildConfig) {
|
||||
this.repository = repository;
|
||||
this.guildConfig = guildConfig;
|
||||
this.resetService = new LevelResetService(repository, guildConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -54,104 +43,29 @@ public class ResetLevelsCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
// Проверка прав администратора
|
||||
if (context.getAuthor() == null
|
||||
|| !context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("❌ Недостаточно прав")
|
||||
.withDescription("Эта команда доступна только администраторам.")
|
||||
.withColor(Color.RED)
|
||||
.build())
|
||||
.queue();
|
||||
context.getChannel().sendMessageEmbeds(ResetLevelsEmbeds.noPermission()).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка, была ли команда уже выполнена
|
||||
String guildId = context.getGuild().getId();
|
||||
String resetValue = guildConfig.get(guildId, RESET_KEY);
|
||||
boolean alreadyReset = "true".equals(resetValue);
|
||||
|
||||
if (alreadyReset) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("⚠️ Команда уже выполнена")
|
||||
.withDescription(
|
||||
"Сброс уровней уже был выполнен ранее."
|
||||
+ "\nЭта команда одноразовая и не может быть использована повторно.")
|
||||
.withColor(Color.ORANGE)
|
||||
.addField(
|
||||
"💡 Информация",
|
||||
"Если нужно сбросить уровни снова, обратитесь к разработчику бота.",
|
||||
false)
|
||||
.build())
|
||||
.queue();
|
||||
if (resetService.isAlreadyReset(guildId)) {
|
||||
context.getChannel().sendMessageEmbeds(ResetLevelsEmbeds.alreadyReset()).queue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем предупреждение с кнопками подтверждения
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("⚠️ ВНИМАНИЕ: Сброс уровней")
|
||||
.withDescription(
|
||||
"Вы собираетесь сбросить **уровни и XP** всех пользователей на сервере.\n\n"
|
||||
+ "**Что будет сброшено:**\n"
|
||||
+ "• Уровень → 0\n"
|
||||
+ "• XP → 0\n\n"
|
||||
+ "**Что НЕ будет затронуто:**\n"
|
||||
+ "• Время в голосовых каналах\n"
|
||||
+ "• Количество сообщений\n"
|
||||
+ "• Репутация\n\n"
|
||||
+ "**⚠️ Это действие НЕОБРАТИМО и может быть выполнено только ОДИН РАЗ!**")
|
||||
.withColor(Color.RED)
|
||||
.addField(
|
||||
"📊 Новая система уровней",
|
||||
"После сброса будет использоваться новая формула:\n"
|
||||
+ "• XP для уровня N = 200 × N\n"
|
||||
+ "• Пример: для 15 уровня нужно 22,800 XP",
|
||||
false)
|
||||
.withFooter("Подтвердите действие нажатием кнопки ниже")
|
||||
.build())
|
||||
.addComponents(
|
||||
ActionRow.of(
|
||||
Button.success(
|
||||
"reset_levels_confirm:" + context.getAuthor().getId(), "✅ Подтвердить сброс"),
|
||||
Button.danger("reset_levels_cancel:" + context.getAuthor().getId(), "❌ Отмена")))
|
||||
.sendMessageEmbeds(ResetLevelsEmbeds.confirmation())
|
||||
.addComponents(ResetLevelsEmbeds.confirmationButtons(context.getAuthor().getId()))
|
||||
.queue();
|
||||
}
|
||||
|
||||
/** Выполняет сброс уровней (вызывается из ButtonHandler) */
|
||||
/** Выполняет сброс уровней (вызывается из ButtonHandler). */
|
||||
public void executeReset(final String guildId, final String userId) {
|
||||
try {
|
||||
LoggerDecorator.warn(logger, "Начало сброса уровней для гильдии: " + guildId);
|
||||
LoggerDecorator.warn(logger, "Инициатор: " + userId);
|
||||
|
||||
// Получаем всех пользователей (без guildId - метод возвращает всех)
|
||||
var allStats = repository.getAllStats();
|
||||
int resetCount = 0;
|
||||
|
||||
// Фильтруем по guildId и сбрасываем только XP и уровень
|
||||
for (var entry : allStats.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
// Ключ формата "guildId:userId"
|
||||
if (key.startsWith(guildId + ":")) {
|
||||
var stats = new UserStats.Builder(entry.getValue()).totalXP(0).level(0).build();
|
||||
repository.save(stats);
|
||||
resetCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Помечаем что команда выполнена
|
||||
guildConfig.set(guildId, RESET_KEY, "true");
|
||||
|
||||
LoggerDecorator.success(logger, "Сброс завершён. Обработано пользователей: " + resetCount);
|
||||
|
||||
resetService.resetLevels(guildId, userId);
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при сбросе уровней", e);
|
||||
throw new RuntimeException("Ошибка при сбросе уровней: " + e.getMessage(), e);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package creator.loki.feature.level.commands.admin;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
/** Построение embed-сообщений для команды сброса уровней. */
|
||||
public final class ResetLevelsEmbeds {
|
||||
private ResetLevelsEmbeds() {}
|
||||
|
||||
public static MessageEmbed noPermission() {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("❌ Недостаточно прав")
|
||||
.withDescription("Эта команда доступна только администраторам.")
|
||||
.withColor(Color.RED)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MessageEmbed alreadyReset() {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("⚠️ Команда уже выполнена")
|
||||
.withDescription(
|
||||
"Сброс уровней уже был выполнен ранее."
|
||||
+ "\nЭта команда одноразовая и не может быть использована повторно.")
|
||||
.withColor(Color.ORANGE)
|
||||
.addField(
|
||||
"💡 Информация",
|
||||
"Если нужно сбросить уровни снова, обратитесь к разработчику бота.",
|
||||
false)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MessageEmbed confirmation() {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("⚠️ ВНИМАНИЕ: Сброс уровней")
|
||||
.withDescription(
|
||||
"Вы собираетесь сбросить **уровни и XP** всех пользователей на сервере.\n\n"
|
||||
+ "**Что будет сброшено:**\n"
|
||||
+ "• Уровень → 0\n"
|
||||
+ "• XP → 0\n\n"
|
||||
+ "**Что НЕ будет затронуто:**\n"
|
||||
+ "• Время в голосовых каналах\n"
|
||||
+ "• Количество сообщений\n"
|
||||
+ "• Репутация\n\n"
|
||||
+ "**⚠️ Это действие НЕОБРАТИМО и может быть выполнено только ОДИН РАЗ!**")
|
||||
.withColor(Color.RED)
|
||||
.addField(
|
||||
"📊 Новая система уровней",
|
||||
"После сброса будет использоваться новая формула:\n"
|
||||
+ "• XP для уровня N = 200 × N\n"
|
||||
+ "• Пример: для 15 уровня нужно 22,800 XP",
|
||||
false)
|
||||
.withFooter("Подтвердите действие нажатием кнопки ниже")
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ActionRow confirmationButtons(final String authorId) {
|
||||
return ActionRow.of(
|
||||
Button.success("reset_levels_confirm:" + authorId, "✅ Подтвердить сброс"),
|
||||
Button.danger("reset_levels_cancel:" + authorId, "❌ Отмена"));
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ public class UserStats {
|
|||
this.lastVoiceJoin = 0;
|
||||
}
|
||||
|
||||
private UserStats(final Builder builder) {
|
||||
UserStats(final UserStatsBuilder builder) {
|
||||
this.userId = builder.userId;
|
||||
this.level = builder.level;
|
||||
this.totalXP = builder.totalXP;
|
||||
|
|
@ -99,62 +99,14 @@ public class UserStats {
|
|||
userId, level, totalXP, messageCount, voiceMinutes);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String userId;
|
||||
private int level;
|
||||
private int totalXP;
|
||||
private int messageCount;
|
||||
private long voiceMinutes;
|
||||
private long lastMessageTime;
|
||||
private long lastVoiceJoin;
|
||||
|
||||
public static class Builder extends UserStatsBuilder {
|
||||
public Builder() {}
|
||||
|
||||
public Builder(final UserStats existing) {
|
||||
this.userId = existing.userId;
|
||||
this.level = existing.level;
|
||||
this.totalXP = existing.totalXP;
|
||||
this.messageCount = existing.messageCount;
|
||||
this.voiceMinutes = existing.voiceMinutes;
|
||||
this.lastMessageTime = existing.lastMessageTime;
|
||||
this.lastVoiceJoin = existing.lastVoiceJoin;
|
||||
}
|
||||
|
||||
public Builder userId(String userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder level(int level) {
|
||||
this.level = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder totalXP(int totalXP) {
|
||||
this.totalXP = totalXP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder messageCount(int messageCount) {
|
||||
this.messageCount = messageCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder voiceMinutes(long voiceMinutes) {
|
||||
this.voiceMinutes = voiceMinutes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lastMessageTime(long lastMessageTime) {
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lastVoiceJoin(long lastVoiceJoin) {
|
||||
this.lastVoiceJoin = lastVoiceJoin;
|
||||
return this;
|
||||
super(existing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserStats build() {
|
||||
return new UserStats(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package creator.loki.feature.level.domain;
|
||||
|
||||
/** Флюент-билдер для {@link UserStats}. */
|
||||
public class UserStatsBuilder {
|
||||
protected String userId;
|
||||
protected int level;
|
||||
protected int totalXP;
|
||||
protected int messageCount;
|
||||
protected long voiceMinutes;
|
||||
protected long lastMessageTime;
|
||||
protected long lastVoiceJoin;
|
||||
|
||||
public UserStatsBuilder() {}
|
||||
|
||||
protected UserStatsBuilder(final UserStats existing) {
|
||||
this.userId = existing.getUserId();
|
||||
this.level = existing.getLevel();
|
||||
this.totalXP = existing.getTotalXP();
|
||||
this.messageCount = existing.getMessageCount();
|
||||
this.voiceMinutes = existing.getVoiceMinutes();
|
||||
this.lastMessageTime = existing.getLastMessageTime();
|
||||
this.lastVoiceJoin = existing.getLastVoiceJoin();
|
||||
}
|
||||
|
||||
public UserStatsBuilder userId(String userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder level(int level) {
|
||||
this.level = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder totalXP(int totalXP) {
|
||||
this.totalXP = totalXP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder messageCount(int messageCount) {
|
||||
this.messageCount = messageCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder voiceMinutes(long voiceMinutes) {
|
||||
this.voiceMinutes = voiceMinutes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder lastMessageTime(long lastMessageTime) {
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStatsBuilder lastVoiceJoin(long lastVoiceJoin) {
|
||||
this.lastVoiceJoin = lastVoiceJoin;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserStats build() {
|
||||
return new UserStats(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package creator.loki.feature.level.service;
|
||||
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.repository.UserStatsRepository;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Выполнение одноразового сброса уровней и XP всех пользователей. */
|
||||
public class LevelResetService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LevelResetService.class);
|
||||
private static final String RESET_KEY = "levels_reset_executed";
|
||||
|
||||
private final UserStatsRepository repository;
|
||||
private final GuildConfigRepository guildConfig;
|
||||
|
||||
public LevelResetService(UserStatsRepository repository, GuildConfigRepository guildConfig) {
|
||||
this.repository = repository;
|
||||
this.guildConfig = guildConfig;
|
||||
}
|
||||
|
||||
public boolean isAlreadyReset(final String guildId) {
|
||||
return "true".equals(guildConfig.get(guildId, RESET_KEY));
|
||||
}
|
||||
|
||||
public int resetLevels(final String guildId, final String userId) {
|
||||
LoggerDecorator.warn(logger, "Начало сброса уровней для гильдии: " + guildId);
|
||||
LoggerDecorator.warn(logger, "Инициатор: " + userId);
|
||||
|
||||
var allStats = repository.getAllStats();
|
||||
int resetCount = 0;
|
||||
|
||||
for (var entry : allStats.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (key.startsWith(guildId + ":")) {
|
||||
var stats = new UserStats.Builder(entry.getValue()).totalXP(0).level(0).build();
|
||||
repository.save(stats);
|
||||
resetCount++;
|
||||
}
|
||||
}
|
||||
|
||||
guildConfig.set(guildId, RESET_KEY, "true");
|
||||
LoggerDecorator.success(logger, "Сброс завершён. Обработано пользователей: " + resetCount);
|
||||
return resetCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package creator.loki.feature.logging;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
final class LogSetupEmbeds {
|
||||
private LogSetupEmbeds() {}
|
||||
|
||||
static MessageEmbed moderationChannelSet(final TextChannel channel) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Канал логов модерации установлен")
|
||||
.withDescription("Логи модерации будут отправляться в " + channel.getAsMention())
|
||||
.addField(
|
||||
"Что логируется",
|
||||
"• Удаление сообщений\n"
|
||||
+ "• Баны/разбаны\n"
|
||||
+ "• Кики\n"
|
||||
+ "• Муты/размуты\n"
|
||||
+ "• Отключение микрофона/звука",
|
||||
false)
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed levelUpChannelSet(final TextChannel channel) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Канал уведомлений о level up установлен")
|
||||
.withDescription(
|
||||
"Уведомления о повышении уровня будут отправляться в " + channel.getAsMention())
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed info(final String moderationChannelId, final String levelupChannelId) {
|
||||
var embed =
|
||||
EmbedFactory.create().withTitle("📋 Конфигурация логирования").withColor(Color.BLUE);
|
||||
|
||||
if (moderationChannelId != null) {
|
||||
embed.addField("Логи модерации", "<#" + moderationChannelId + ">", false);
|
||||
} else {
|
||||
embed.addField("Логи модерации", "❌ Не установлен", false);
|
||||
}
|
||||
|
||||
if (levelupChannelId != null) {
|
||||
embed.addField("Уведомления level up", "<#" + levelupChannelId + ">", false);
|
||||
} else {
|
||||
embed.addField("Уведомления level up", "❌ Не установлен", false);
|
||||
}
|
||||
|
||||
return embed.build();
|
||||
}
|
||||
|
||||
static MessageEmbed help() {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("📋 Настройка логирования")
|
||||
.withDescription(
|
||||
"""
|
||||
**Команды:**
|
||||
• `!log-setup moderation` - `#канал` или числовой id
|
||||
• `!log-setup levelup` - `#канал` или id
|
||||
• `!log-setup info` - текущая конфигурация
|
||||
|
||||
Только для администраторов.
|
||||
""")
|
||||
.withColor(Color.BLUE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
package creator.loki.feature.logging;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.util.GuildChannelResolve;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
|
|
@ -62,13 +60,44 @@ public class LogSetupPrefixCommand implements Command {
|
|||
}
|
||||
|
||||
private void handleModeration(final CommandContext context, final List<String> args) {
|
||||
TextChannel channel = requireChannel(context, args);
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
configRepo.set("moderation.log_channel_id", channel.getId());
|
||||
context.getChannel().sendMessageEmbeds(LogSetupEmbeds.moderationChannelSet(channel)).queue();
|
||||
}
|
||||
|
||||
private void handleLevelUp(final CommandContext context, final List<String> args) {
|
||||
TextChannel channel = requireChannel(context, args);
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
configRepo.set("levelup.notification_channel_id", channel.getId());
|
||||
context.getChannel().sendMessageEmbeds(LogSetupEmbeds.levelUpChannelSet(channel)).queue();
|
||||
}
|
||||
|
||||
private void handleInfo(final CommandContext context) {
|
||||
String moderationChannelId = configRepo.get("moderation.log_channel_id");
|
||||
String levelupChannelId = configRepo.get("levelup.notification_channel_id");
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(LogSetupEmbeds.info(moderationChannelId, levelupChannelId))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void sendHelp(final CommandContext context) {
|
||||
context.getChannel().sendMessageEmbeds(LogSetupEmbeds.help()).queue();
|
||||
}
|
||||
|
||||
private TextChannel requireChannel(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage(
|
||||
"❌ Укажите текстовый канал: `#канал`, `<#id>` или id. Пример: `!log-setup moderation 123456789012345678`")
|
||||
.queue();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
TextChannel channel =
|
||||
GuildChannelResolve.text(context.getGuild(), context.getMessage(), args, 1);
|
||||
|
|
@ -78,101 +107,7 @@ public class LogSetupPrefixCommand implements Command {
|
|||
.sendMessage(
|
||||
"❌ Не найден текстовый канал. Укажите `#канал`, `<#id>` или id из 17-20 цифр.")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
configRepo.set("moderation.log_channel_id", channel.getId());
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Канал логов модерации установлен")
|
||||
.withDescription("Логи модерации будут отправляться в " + channel.getAsMention())
|
||||
.addField(
|
||||
"Что логируется",
|
||||
"• Удаление сообщений\n"
|
||||
+ "• Баны/разбаны\n"
|
||||
+ "• Кики\n"
|
||||
+ "• Муты/размуты\n"
|
||||
+ "• Отключение микрофона/звука",
|
||||
false)
|
||||
.withColor(Color.GREEN)
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleLevelUp(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage(
|
||||
"❌ Укажите текстовый канал: `#канал`, `<#id>` или id. Пример: `!log-setup levelup 123456789012345678`")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
TextChannel channel =
|
||||
GuildChannelResolve.text(context.getGuild(), context.getMessage(), args, 1);
|
||||
if (channel == null) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage(
|
||||
"❌ Не найден текстовый канал. Укажите `#канал`, `<#id>` или id из 17-20 цифр.")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
configRepo.set("levelup.notification_channel_id", channel.getId());
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Канал уведомлений о level up установлен")
|
||||
.withDescription(
|
||||
"Уведомления о повышении уровня будут отправляться в " + channel.getAsMention())
|
||||
.withColor(Color.GREEN)
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleInfo(final CommandContext context) {
|
||||
String moderationChannelId = configRepo.get("moderation.log_channel_id");
|
||||
String levelupChannelId = configRepo.get("levelup.notification_channel_id");
|
||||
|
||||
var embed =
|
||||
EmbedFactory.create().withTitle("📋 Конфигурация логирования").withColor(Color.BLUE);
|
||||
|
||||
if (moderationChannelId != null) {
|
||||
embed.addField("Логи модерации", "<#" + moderationChannelId + ">", false);
|
||||
} else {
|
||||
embed.addField("Логи модерации", "❌ Не установлен", false);
|
||||
}
|
||||
|
||||
if (levelupChannelId != null) {
|
||||
embed.addField("Уведомления level up", "<#" + levelupChannelId + ">", false);
|
||||
} else {
|
||||
embed.addField("Уведомления level up", "❌ Не установлен", false);
|
||||
}
|
||||
|
||||
context.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||
}
|
||||
|
||||
private void sendHelp(final CommandContext context) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("📋 Настройка логирования")
|
||||
.withDescription(
|
||||
"""
|
||||
**Команды:**
|
||||
• `!log-setup moderation` - `#канал` или числовой id
|
||||
• `!log-setup levelup` - `#канал` или id
|
||||
• `!log-setup info` - текущая конфигурация
|
||||
|
||||
Только для администраторов.
|
||||
""")
|
||||
.withColor(Color.BLUE)
|
||||
.build())
|
||||
.queue();
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
package creator.loki.feature.metrics;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.BindException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.feature.tiktok.TempFile;
|
||||
import creator.loki.feature.tiktok.TempFileManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -26,16 +19,14 @@ public class HealthHttpServer implements Lifecycle {
|
|||
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
||||
|
||||
private final BotConfig config;
|
||||
private final MetricsCollector metrics;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final HttpEndpoints endpoints;
|
||||
private HttpServer server;
|
||||
|
||||
@Inject
|
||||
public HealthHttpServer(
|
||||
BotConfig config, MetricsCollector metrics, TempFileManager tempFileManager) {
|
||||
this.config = config;
|
||||
this.metrics = metrics;
|
||||
this.tempFileManager = tempFileManager;
|
||||
this.endpoints = new HttpEndpoints(metrics, tempFileManager, logger);
|
||||
}
|
||||
|
||||
public synchronized void start() {
|
||||
|
|
@ -49,8 +40,8 @@ public class HealthHttpServer implements Lifecycle {
|
|||
}
|
||||
try {
|
||||
server = bindHealthServer(port);
|
||||
server.createContext("/health", this::handleHealth);
|
||||
server.createContext("/d/", this::handleDownload);
|
||||
server.createContext("/health", endpoints::handleHealth);
|
||||
server.createContext("/d/", endpoints::handleDownload);
|
||||
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
server.start();
|
||||
int bound = server.getAddress().getPort();
|
||||
|
|
@ -82,85 +73,6 @@ public class HealthHttpServer implements Lifecycle {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleHealth(final HttpExchange exchange) {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
if (!"/health".equals(path)) {
|
||||
exchange.sendResponseHeaders(404, -1);
|
||||
return;
|
||||
}
|
||||
String body = metrics.buildHealthJson();
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка ответа /health", e);
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, -1);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownload(final HttpExchange exchange) {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String key = path.substring("/d/".length());
|
||||
if (key.isEmpty()) {
|
||||
exchange.sendResponseHeaders(400, -1);
|
||||
return;
|
||||
}
|
||||
TempFile entry = tempFileManager.getEntry(key);
|
||||
if (entry == null) {
|
||||
String body = "File not found or expired";
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
|
||||
exchange.sendResponseHeaders(404, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Path filePath = tempFileManager.getFilePath(entry);
|
||||
if (!Files.exists(filePath)) {
|
||||
tempFileManager.getEntry(key);
|
||||
exchange.sendResponseHeaders(404, -1);
|
||||
return;
|
||||
}
|
||||
long fileSize = Files.size(filePath);
|
||||
exchange.getResponseHeaders().add("Content-Type", "video/mp4");
|
||||
exchange
|
||||
.getResponseHeaders()
|
||||
.add("Content-Disposition", "attachment; filename=\"" + entry.filename() + "\"");
|
||||
exchange.sendResponseHeaders(200, fileSize);
|
||||
try (InputStream is = Files.newInputStream(filePath);
|
||||
OutputStream os = exchange.getResponseBody()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка ответа /d/", e);
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, -1);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void shutdown() {
|
||||
if (server != null) {
|
||||
|
|
|
|||
111
src/main/java/creator/loki/feature/metrics/HttpEndpoints.java
Normal file
111
src/main/java/creator/loki/feature/metrics/HttpEndpoints.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package creator.loki.feature.metrics;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import creator.loki.feature.tiktok.TempFile;
|
||||
import creator.loki.feature.tiktok.TempFileManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/** HTTP-обработчики для /health и /d/ эндпоинтов. */
|
||||
public class HttpEndpoints {
|
||||
private final MetricsCollector metrics;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final Logger logger;
|
||||
|
||||
public HttpEndpoints(MetricsCollector metrics, TempFileManager tempFileManager, Logger logger) {
|
||||
this.metrics = metrics;
|
||||
this.tempFileManager = tempFileManager;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public void handleHealth(final HttpExchange exchange) {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
if (!"/health".equals(exchange.getRequestURI().getPath())) {
|
||||
exchange.sendResponseHeaders(404, -1);
|
||||
return;
|
||||
}
|
||||
sendBytes(
|
||||
exchange,
|
||||
200,
|
||||
metrics.buildHealthJson().getBytes(StandardCharsets.UTF_8),
|
||||
"application/json; charset=utf-8");
|
||||
} catch (Exception e) {
|
||||
respondWithError(exchange, "Ошибка ответа /health", e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void handleDownload(final HttpExchange exchange) {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String key = path.substring("/d/".length());
|
||||
if (key.isEmpty()) {
|
||||
exchange.sendResponseHeaders(400, -1);
|
||||
return;
|
||||
}
|
||||
TempFile entry = tempFileManager.getEntry(key);
|
||||
if (entry == null) {
|
||||
sendBytes(
|
||||
exchange,
|
||||
404,
|
||||
"File not found or expired".getBytes(StandardCharsets.UTF_8),
|
||||
"text/plain; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
Path filePath = tempFileManager.getFilePath(entry);
|
||||
if (!Files.exists(filePath)) {
|
||||
exchange.sendResponseHeaders(404, -1);
|
||||
return;
|
||||
}
|
||||
long fileSize = Files.size(filePath);
|
||||
exchange.getResponseHeaders().add("Content-Type", "video/mp4");
|
||||
exchange
|
||||
.getResponseHeaders()
|
||||
.add("Content-Disposition", "attachment; filename=\"" + entry.filename() + "\"");
|
||||
exchange.sendResponseHeaders(200, fileSize);
|
||||
try (InputStream is = Files.newInputStream(filePath);
|
||||
OutputStream os = exchange.getResponseBody()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
respondWithError(exchange, "Ошибка ответа /d/", e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBytes(
|
||||
final HttpExchange exchange, final int status, final byte[] bytes, final String contentType)
|
||||
throws IOException {
|
||||
exchange.getResponseHeaders().add("Content-Type", contentType);
|
||||
exchange.sendResponseHeaders(status, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void respondWithError(
|
||||
final HttpExchange exchange, final String message, final Exception e) {
|
||||
LoggerDecorator.error(logger, message, e);
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, -1);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package creator.loki.feature.moderation.commands.punish;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.util.MuteDurationParser;
|
||||
|
||||
final class MuteArgs {
|
||||
private MuteArgs() {}
|
||||
|
||||
record Parsed(MuteDurationParser.MuteDuration dur, String reason) {}
|
||||
|
||||
static Parsed parse(final List<String> args) {
|
||||
String durToken = findDurationToken(args);
|
||||
if (durToken == null) {
|
||||
return null;
|
||||
}
|
||||
return new Parsed(MuteDurationParser.parse(durToken), extractReason(args, durToken));
|
||||
}
|
||||
|
||||
private static String findDurationToken(final List<String> args) {
|
||||
for (String a : args) {
|
||||
if (a.startsWith("<@")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
MuteDurationParser.parse(a);
|
||||
return a;
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractReason(final List<String> args, final String durToken) {
|
||||
boolean past = false;
|
||||
var sb = new StringBuilder();
|
||||
for (String a : args) {
|
||||
if (a.startsWith("<@")) {
|
||||
continue;
|
||||
}
|
||||
if (!past) {
|
||||
if (a.equals(durToken)) {
|
||||
past = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(a);
|
||||
}
|
||||
String tmp = sb.toString().trim();
|
||||
return tmp.isEmpty() ? "Без причины" : tmp;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ import net.dv8tion.jda.api.entities.Role;
|
|||
|
||||
public class MuteCommand implements Command {
|
||||
private final MuteNotifier notifier;
|
||||
private final ConfigRepository configRepo;
|
||||
private final MuteRoleService muteRoleService;
|
||||
private final MuteScheduler muteScheduler;
|
||||
|
||||
|
|
@ -28,7 +27,6 @@ public class MuteCommand implements Command {
|
|||
final MuteRoleService mrs,
|
||||
final MuteScheduler ms) {
|
||||
this.notifier = new MuteNotifier(c, a, cr, ms);
|
||||
this.configRepo = cr;
|
||||
this.muteRoleService = mrs;
|
||||
this.muteScheduler = ms;
|
||||
}
|
||||
|
|
@ -42,10 +40,13 @@ public class MuteCommand implements Command {
|
|||
public String getDescription() {
|
||||
return "Выдать мут на время или навсегда";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "mute <@user> <5m|1h|1d|perm> [причина]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MODERATION;
|
||||
}
|
||||
|
|
@ -71,8 +72,9 @@ public class MuteCommand implements Command {
|
|||
if (!validateMute(ctx, target, mod, g)) {
|
||||
return;
|
||||
}
|
||||
var parsed = parseDurationAndReason(ctx);
|
||||
var parsed = MuteArgs.parse(ctx.getArgs());
|
||||
if (parsed == null) {
|
||||
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
||||
return;
|
||||
}
|
||||
Role muteRole;
|
||||
|
|
@ -86,20 +88,18 @@ public class MuteCommand implements Command {
|
|||
err(ctx, "Подними роль бота выше роли **Мут**");
|
||||
return;
|
||||
}
|
||||
long caseId = notifier.createMuteCase(g, target, mod, parsed.dur, parsed.reason);
|
||||
long caseId = notifier.createMuteCase(g, target, mod, parsed.dur(), parsed.reason());
|
||||
if (caseId < 0) {
|
||||
err(ctx, "Ошибка записи кейса");
|
||||
return;
|
||||
}
|
||||
applyMuteRole(
|
||||
new MuteContext(ctx, g, target, muteRole, caseId),
|
||||
new MuteDetails(parsed.dur, parsed.reason, mod));
|
||||
new MuteDetails(parsed.dur(), parsed.reason(), mod));
|
||||
}
|
||||
|
||||
private boolean validateMute(
|
||||
final CommandContext ctx,
|
||||
final Member target,
|
||||
final Member mod,
|
||||
final Guild g) {
|
||||
final CommandContext ctx, final Member target, final Member mod, final Guild g) {
|
||||
if (target.getId().equals(mod.getId())) {
|
||||
err(ctx, "Нельзя замутить себя");
|
||||
return false;
|
||||
|
|
@ -115,54 +115,6 @@ public class MuteCommand implements Command {
|
|||
return true;
|
||||
}
|
||||
|
||||
private record ParsedDuration(MuteDurationParser.MuteDuration dur, String reason) {}
|
||||
private ParsedDuration parseDurationAndReason(final CommandContext ctx) {
|
||||
String durToken = findDurationToken(ctx.getArgs());
|
||||
if (durToken == null) {
|
||||
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
||||
return null;
|
||||
}
|
||||
MuteDurationParser.MuteDuration dur = MuteDurationParser.parse(durToken);
|
||||
String reason = extractReason(ctx.getArgs(), durToken);
|
||||
return new ParsedDuration(dur, reason);
|
||||
}
|
||||
|
||||
private String findDurationToken(final java.util.List<String> args) {
|
||||
for (String a : args) {
|
||||
if (a.startsWith("<@")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
MuteDurationParser.parse(a);
|
||||
return a;
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractReason(final java.util.List<String> args, final String durToken) {
|
||||
boolean past = false;
|
||||
var sb = new StringBuilder();
|
||||
for (String a : args) {
|
||||
if (a.startsWith("<@")) {
|
||||
continue;
|
||||
}
|
||||
if (!past) {
|
||||
if (a.equals(durToken)) {
|
||||
past = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(a);
|
||||
}
|
||||
String tmp = sb.toString().trim();
|
||||
return tmp.isEmpty() ? "Без причины" : tmp;
|
||||
}
|
||||
|
||||
private record MuteContext(
|
||||
CommandContext ctx, Guild g, Member target, Role muteRole, long caseId) {}
|
||||
|
||||
|
|
@ -178,8 +130,7 @@ public class MuteCommand implements Command {
|
|||
var mod = md.mod();
|
||||
var ctx = mc.ctx();
|
||||
boolean permanent = dur.permanent();
|
||||
long secs = dur.secondsTotal();
|
||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + dur.secondsTotal() * 1000;
|
||||
g.addRoleToMember(target, muteRole)
|
||||
.queue(
|
||||
ok -> {
|
||||
|
|
@ -190,10 +141,9 @@ public class MuteCommand implements Command {
|
|||
.queue();
|
||||
notifier.sendMuteLogEmbed(g, target, mod, caseId, dur, reason, untilMs);
|
||||
},
|
||||
err2 -> {
|
||||
err(ctx, "Ошибка выдачи роли");
|
||||
});
|
||||
err2 -> err(ctx, "Ошибка выдачи роли"));
|
||||
}
|
||||
|
||||
private static void err(final CommandContext ctx, final String msg) {
|
||||
ModerationHelper.error(ctx, msg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,7 @@ class MuteNotifier {
|
|||
}
|
||||
|
||||
long createMuteCase(
|
||||
Guild g,
|
||||
Member target,
|
||||
Member mod,
|
||||
MuteDurationParser.MuteDuration dur,
|
||||
String reason) {
|
||||
Guild g, Member target, Member mod, MuteDurationParser.MuteDuration dur, String reason) {
|
||||
boolean permanent = dur.permanent();
|
||||
long secs = dur.secondsTotal();
|
||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||
|
|
@ -105,18 +101,11 @@ class MuteNotifier {
|
|||
new EmbedBuilder()
|
||||
.setTitle("🔇 Мут")
|
||||
.setColor(Color.DARK_GRAY)
|
||||
.addField(
|
||||
"Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
||||
.addField("Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
||||
.addField("Модератор", mod.getAsMention(), false)
|
||||
.addField("Кейс", "`" + caseId + "`", true)
|
||||
.addField(
|
||||
"Срок",
|
||||
permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>",
|
||||
true)
|
||||
.addField(
|
||||
"Причина",
|
||||
ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH),
|
||||
false)
|
||||
.addField("Срок", permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>", true)
|
||||
.addField("Причина", ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH), false)
|
||||
.setTimestamp(OffsetDateTime.now())
|
||||
.build())
|
||||
.queue();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
package creator.loki.feature.moderation.commands.punish;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
|
|
@ -15,15 +11,14 @@ 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 net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
|
||||
public class UnmuteCommand implements Command {
|
||||
private final ModerationCaseRepository cases;
|
||||
private final ActiveMuteRepository activeMutes;
|
||||
private final ConfigRepository configRepo;
|
||||
private final GuildConfigRepository guildConfig;
|
||||
private final MuteScheduler muteScheduler;
|
||||
private final UnmuteNotifier notifier;
|
||||
|
||||
public UnmuteCommand(
|
||||
final ModerationCaseRepository c,
|
||||
|
|
@ -33,9 +28,9 @@ public class UnmuteCommand implements Command {
|
|||
final MuteScheduler ms) {
|
||||
cases = c;
|
||||
activeMutes = a;
|
||||
configRepo = cr;
|
||||
guildConfig = gc;
|
||||
muteScheduler = ms;
|
||||
notifier = new UnmuteNotifier(cr);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -105,65 +100,10 @@ public class UnmuteCommand implements Command {
|
|||
long caseId = cases.insert(g.getId(), "UNMUTE", t.getId(), mod.getId(), reason, null, null);
|
||||
g.removeRoleFromMember(t, muteRole)
|
||||
.queue(
|
||||
ok -> onMuteRemoved(ctx, g, t, mod, caseId, reason),
|
||||
ok -> notifier.notifyMuteRemoved(ctx, g, t, mod, caseId, reason),
|
||||
err2 -> err(ctx, "Ошибка снятия роли"));
|
||||
}
|
||||
|
||||
private void onMuteRemoved(
|
||||
final CommandContext ctx,
|
||||
final net.dv8tion.jda.api.entities.Guild g,
|
||||
final net.dv8tion.jda.api.entities.Member t,
|
||||
final net.dv8tion.jda.api.entities.Member mod,
|
||||
final long caseId,
|
||||
final String reason) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Мут снят")
|
||||
.withColor(Color.GREEN)
|
||||
.withDescription(
|
||||
"**С кого:** "
|
||||
+ t.getAsMention()
|
||||
+ (caseId > 0 ? "\n**Кейс:** `" + caseId + "`" : "")
|
||||
+ "\n**Причина:** "
|
||||
+ trunc(reason, 500))
|
||||
.build())
|
||||
.queue();
|
||||
sendModLog(g, t, mod, caseId, reason);
|
||||
}
|
||||
|
||||
private void sendModLog(
|
||||
final net.dv8tion.jda.api.entities.Guild g,
|
||||
final net.dv8tion.jda.api.entities.Member t,
|
||||
final net.dv8tion.jda.api.entities.Member mod,
|
||||
final long caseId,
|
||||
final String reason) {
|
||||
var logId = configRepo.get("moderation.log_channel_id");
|
||||
if (logId == null) {
|
||||
return;
|
||||
}
|
||||
var log = g.getTextChannelById(logId);
|
||||
if (log == null) {
|
||||
return;
|
||||
}
|
||||
var eb =
|
||||
new EmbedBuilder()
|
||||
.setTitle("🔊 Размут")
|
||||
.setColor(Color.GREEN)
|
||||
.addField("Участник", t.getAsMention() + " (`" + t.getId() + "`)", false)
|
||||
.addField("Модератор", mod.getAsMention(), false);
|
||||
if (caseId > 0) {
|
||||
eb.addField("Кейс", "`" + caseId + "`", true);
|
||||
}
|
||||
eb.addField("Причина", trunc(reason, 1000), false)
|
||||
.setTimestamp(OffsetDateTime.now());
|
||||
log.sendMessageEmbeds(eb.build()).queue(s -> {}, e -> {});
|
||||
}
|
||||
|
||||
private static String trunc(final String s, final int max) {
|
||||
return ModerationHelper.trunc(s, max);
|
||||
}
|
||||
|
||||
private static void err(final CommandContext ctx, final String msg) {
|
||||
ModerationHelper.error(ctx, msg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package creator.loki.feature.moderation.commands.punish;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
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 net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
class UnmuteNotifier {
|
||||
private static final int MAX_REASON_LENGTH = 500;
|
||||
private static final int MAX_LOG_REASON_LENGTH = 1000;
|
||||
|
||||
private final ConfigRepository configRepo;
|
||||
|
||||
UnmuteNotifier(ConfigRepository configRepo) {
|
||||
this.configRepo = configRepo;
|
||||
}
|
||||
|
||||
void notifyMuteRemoved(
|
||||
final CommandContext ctx,
|
||||
final Guild g,
|
||||
final Member t,
|
||||
final Member mod,
|
||||
final long caseId,
|
||||
final String reason) {
|
||||
ctx.getChannel().sendMessageEmbeds(buildConfirmEmbed(t, caseId, reason)).queue();
|
||||
sendModLog(g, t, mod, caseId, reason);
|
||||
}
|
||||
|
||||
private MessageEmbed buildConfirmEmbed(final Member t, final long caseId, final String reason) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Мут снят")
|
||||
.withColor(Color.GREEN)
|
||||
.withDescription(
|
||||
"**С кого:** "
|
||||
+ t.getAsMention()
|
||||
+ (caseId > 0 ? "\n**Кейс:** `" + caseId + "`" : "")
|
||||
+ "\n**Причина:** "
|
||||
+ ModerationHelper.trunc(reason, MAX_REASON_LENGTH))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void sendModLog(
|
||||
final Guild g, final Member t, final Member mod, final long caseId, final String reason) {
|
||||
var logId = configRepo.get("moderation.log_channel_id");
|
||||
if (logId == null) {
|
||||
return;
|
||||
}
|
||||
var log = g.getTextChannelById(logId);
|
||||
if (log == null) {
|
||||
return;
|
||||
}
|
||||
var eb =
|
||||
new EmbedBuilder()
|
||||
.setTitle("🔊 Размут")
|
||||
.setColor(Color.GREEN)
|
||||
.addField("Участник", t.getAsMention() + " (`" + t.getId() + "`)", false)
|
||||
.addField("Модератор", mod.getAsMention(), false);
|
||||
if (caseId > 0) {
|
||||
eb.addField("Кейс", "`" + caseId + "`", true);
|
||||
}
|
||||
eb.addField("Причина", ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH), false)
|
||||
.setTimestamp(OffsetDateTime.now());
|
||||
log.sendMessageEmbeds(eb.build()).queue(s -> {}, e -> {});
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,7 @@ class WarnAutoMute {
|
|||
String reason = "Авто-мут: 3+ варна";
|
||||
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
|
||||
long durSec = AUTO_MUTE_HOURS * 3600;
|
||||
long caseId =
|
||||
cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
|
||||
long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
|
||||
if (caseId < 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package creator.loki.feature.moderation.log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.audit.ActionType;
|
||||
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
final class AuditLogs {
|
||||
private AuditLogs() {}
|
||||
|
||||
static AuditLogEntry findEntry(List<AuditLogEntry> l, String targetId, String... key) {
|
||||
var s = l.stream().filter(e -> targetId.equals(e.getTargetId()));
|
||||
if (key.length > 0) {
|
||||
s = s.filter(e -> e.getChangeByKey(key[0]) != null);
|
||||
}
|
||||
return s.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
static String mod(AuditLogEntry e) {
|
||||
return e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
|
||||
}
|
||||
|
||||
static void sendForTarget(
|
||||
Guild g,
|
||||
ActionType type,
|
||||
int limit,
|
||||
String targetId,
|
||||
Function<AuditLogEntry, EmbedBuilder> embed,
|
||||
BiConsumer<Guild, EmbedBuilder> send) {
|
||||
g.retrieveAuditLogs()
|
||||
.type(type)
|
||||
.limit(limit)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry = findEntry(logs, targetId);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
send.accept(g, embed.apply(entry));
|
||||
});
|
||||
}
|
||||
|
||||
static void sendFirst(
|
||||
Guild g,
|
||||
ActionType type,
|
||||
int limit,
|
||||
Function<AuditLogEntry, EmbedBuilder> embed,
|
||||
BiConsumer<Guild, EmbedBuilder> send) {
|
||||
g.retrieveAuditLogs()
|
||||
.type(type)
|
||||
.limit(limit)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry = logs.stream().findFirst().orElse(null);
|
||||
send.accept(g, embed.apply(entry));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package creator.loki.feature.moderation.log;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
|
@ -9,7 +8,6 @@ import creator.loki.core.MessageCache;
|
|||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.audit.ActionType;
|
||||
import net.dv8tion.jda.api.audit.AuditLogEntry;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
|
||||
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
|
||||
|
|
@ -52,120 +50,87 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
@Override
|
||||
public void onMessageDelete(MessageDeleteEvent e) {
|
||||
var content = messageCache.getContent(e.getMessageId());
|
||||
var g = e.getGuild();
|
||||
g.retrieveAuditLogs()
|
||||
.type(ActionType.MESSAGE_DELETE)
|
||||
.limit(5)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry =
|
||||
logs.stream()
|
||||
.filter(log -> e.getMessageId().equals(log.getTargetId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content));
|
||||
});
|
||||
AuditLogs.sendForTarget(
|
||||
e.getGuild(),
|
||||
ActionType.MESSAGE_DELETE,
|
||||
5,
|
||||
e.getMessageId(),
|
||||
entry -> LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
|
||||
var g = e.getGuild();
|
||||
var size = e.getMessageIds().size();
|
||||
g.retrieveAuditLogs()
|
||||
.type(ActionType.MESSAGE_BULK_DELETE)
|
||||
.limit(3)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry = logs.stream().findFirst().orElse(null);
|
||||
send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size));
|
||||
});
|
||||
AuditLogs.sendFirst(
|
||||
e.getGuild(),
|
||||
ActionType.MESSAGE_BULK_DELETE,
|
||||
3,
|
||||
entry -> LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildBan(GuildBanEvent ev) {
|
||||
ev.getGuild()
|
||||
.retrieveAuditLogs()
|
||||
.type(ActionType.BAN)
|
||||
.limit(5)
|
||||
.queue(
|
||||
l -> {
|
||||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
send(
|
||||
ev.getGuild(),
|
||||
LogEmbeds.ban(
|
||||
ev.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl()));
|
||||
});
|
||||
AuditLogs.sendForTarget(
|
||||
ev.getGuild(),
|
||||
ActionType.BAN,
|
||||
5,
|
||||
ev.getUser().getId(),
|
||||
entry ->
|
||||
LogEmbeds.ban(
|
||||
ev.getUser().getAsMention(),
|
||||
AuditLogs.mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl()),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildUnban(GuildUnbanEvent ev) {
|
||||
ev.getGuild()
|
||||
.retrieveAuditLogs()
|
||||
.type(ActionType.UNBAN)
|
||||
.limit(5)
|
||||
.queue(
|
||||
l -> {
|
||||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
send(
|
||||
ev.getGuild(),
|
||||
LogEmbeds.unban(
|
||||
ev.getUser().getAsMention(), mod(entry), ev.getUser().getAvatarUrl()));
|
||||
});
|
||||
AuditLogs.sendForTarget(
|
||||
ev.getGuild(),
|
||||
ActionType.UNBAN,
|
||||
5,
|
||||
ev.getUser().getId(),
|
||||
entry ->
|
||||
LogEmbeds.unban(
|
||||
ev.getUser().getAsMention(), AuditLogs.mod(entry), ev.getUser().getAvatarUrl()),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMemberRemove(GuildMemberRemoveEvent ev) {
|
||||
ev.getGuild()
|
||||
.retrieveAuditLogs()
|
||||
.type(ActionType.KICK)
|
||||
.limit(5)
|
||||
.queue(
|
||||
l -> {
|
||||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
send(
|
||||
ev.getGuild(),
|
||||
LogEmbeds.kick(
|
||||
ev.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl()));
|
||||
});
|
||||
AuditLogs.sendForTarget(
|
||||
ev.getGuild(),
|
||||
ActionType.KICK,
|
||||
5,
|
||||
ev.getUser().getId(),
|
||||
entry ->
|
||||
LogEmbeds.kick(
|
||||
ev.getUser().getAsMention(),
|
||||
AuditLogs.mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl()),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent e) {
|
||||
var muted = e.getNewTimeOutEnd() != null;
|
||||
e.getGuild()
|
||||
.retrieveAuditLogs()
|
||||
.type(ActionType.MEMBER_UPDATE)
|
||||
.limit(5)
|
||||
.queue(
|
||||
l -> {
|
||||
var entry = findEntry(l, e.getUser().getId());
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
send(
|
||||
e.getGuild(),
|
||||
LogEmbeds.timeout(
|
||||
e.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
muted,
|
||||
muted ? e.getNewTimeOutEnd().toEpochSecond() : null,
|
||||
entry.getReason()));
|
||||
});
|
||||
AuditLogs.sendForTarget(
|
||||
e.getGuild(),
|
||||
ActionType.MEMBER_UPDATE,
|
||||
5,
|
||||
e.getUser().getId(),
|
||||
entry ->
|
||||
LogEmbeds.timeout(
|
||||
e.getUser().getAsMention(),
|
||||
AuditLogs.mod(entry),
|
||||
muted,
|
||||
muted ? e.getNewTimeOutEnd().toEpochSecond() : null,
|
||||
entry.getReason()),
|
||||
this::send);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -177,16 +142,4 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent e) {
|
||||
voiceLogger.onGuildVoiceGuildDeafen(e);
|
||||
}
|
||||
|
||||
static AuditLogEntry findEntry(List<AuditLogEntry> l, String targetId, String... key) {
|
||||
var s = l.stream().filter(e -> targetId.equals(e.getTargetId()));
|
||||
if (key.length > 0) {
|
||||
s = s.filter(e -> e.getChangeByKey(key[0]) != null);
|
||||
}
|
||||
return s.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
static String mod(AuditLogEntry e) {
|
||||
return e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,23 +30,23 @@ class VoiceAuditLogger {
|
|||
|
||||
private EmbedBuilder voiceMute(GuildVoiceGuildMuteEvent e, boolean muted, List<AuditLogEntry> l) {
|
||||
var member = e.getMember();
|
||||
var entry = ModerationLogger.findEntry(l, member.getId(), "mute");
|
||||
var entry = AuditLogs.findEntry(l, member.getId(), "mute");
|
||||
return LogEmbeds.voiceMute(
|
||||
member.getAsMention(),
|
||||
e.getVoiceState().getChannel().getAsMention(),
|
||||
muted,
|
||||
entry != null ? ModerationLogger.mod(entry) : null);
|
||||
entry != null ? AuditLogs.mod(entry) : null);
|
||||
}
|
||||
|
||||
private EmbedBuilder voiceDeafen(
|
||||
GuildVoiceGuildDeafenEvent e, boolean deaf, List<AuditLogEntry> l) {
|
||||
var member = e.getMember();
|
||||
var entry = ModerationLogger.findEntry(l, member.getId(), "deaf");
|
||||
var entry = AuditLogs.findEntry(l, member.getId(), "deaf");
|
||||
return LogEmbeds.voiceDeafen(
|
||||
member.getAsMention(),
|
||||
e.getVoiceState().getChannel().getAsMention(),
|
||||
deaf,
|
||||
entry != null ? ModerationLogger.mod(entry) : null);
|
||||
entry != null ? AuditLogs.mod(entry) : null);
|
||||
}
|
||||
|
||||
private void handle(
|
||||
|
|
|
|||
|
|
@ -30,22 +30,23 @@ public class MusicSlashCommands extends ListenerAdapter {
|
|||
this.playHandler = new PlaySlashHandler(playerManager);
|
||||
this.queueHandler = new QueueSlashHandler(playerManager);
|
||||
this.filterHandler = new FilterSlashHandler(playerManager);
|
||||
this.commandMap = Map.ofEntries(
|
||||
Map.entry("play", playHandler::handle),
|
||||
Map.entry("stop", queueHandler::handleStop),
|
||||
Map.entry("skip", queueHandler::handleSkip),
|
||||
Map.entry("queue", queueHandler::handleQueue),
|
||||
Map.entry("nowplaying", queueHandler::handleNowPlaying),
|
||||
Map.entry("volume", queueHandler::handleVolume),
|
||||
Map.entry("pause", queueHandler::handlePause),
|
||||
Map.entry("resume", queueHandler::handleResume),
|
||||
Map.entry("shuffle", queueHandler::handleShuffle),
|
||||
Map.entry("repeat", queueHandler::handleRepeat),
|
||||
Map.entry("bassboost", filterHandler::handleBassboost),
|
||||
Map.entry("nightcore", filterHandler::handleNightcore),
|
||||
Map.entry("vaporwave", filterHandler::handleVaporwave),
|
||||
Map.entry("equalizer", filterHandler::handleEqualizer),
|
||||
Map.entry("reset", filterHandler::handleReset));
|
||||
this.commandMap =
|
||||
Map.ofEntries(
|
||||
Map.entry("play", playHandler::handle),
|
||||
Map.entry("stop", queueHandler::handleStop),
|
||||
Map.entry("skip", queueHandler::handleSkip),
|
||||
Map.entry("queue", queueHandler::handleQueue),
|
||||
Map.entry("nowplaying", queueHandler::handleNowPlaying),
|
||||
Map.entry("volume", queueHandler::handleVolume),
|
||||
Map.entry("pause", queueHandler::handlePause),
|
||||
Map.entry("resume", queueHandler::handleResume),
|
||||
Map.entry("shuffle", queueHandler::handleShuffle),
|
||||
Map.entry("repeat", queueHandler::handleRepeat),
|
||||
Map.entry("bassboost", filterHandler::handleBassboost),
|
||||
Map.entry("nightcore", filterHandler::handleNightcore),
|
||||
Map.entry("vaporwave", filterHandler::handleVaporwave),
|
||||
Map.entry("equalizer", filterHandler::handleEqualizer),
|
||||
Map.entry("reset", filterHandler::handleReset));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
|
||||
final class BandCommand implements Command {
|
||||
private final PlayerManager pm;
|
||||
private final int mode;
|
||||
|
||||
BandCommand(PlayerManager pm, int mode) {
|
||||
this.pm = pm;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return mode == 0 ? "bassboost" : mode == 1 ? "equalizer" : "reset";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return mode == 0
|
||||
? "Bassboost (-25 to 25)"
|
||||
: mode == 1 ? "Set equalizer band (0-14, gain -0.25 to 1.0)" : "Reset all filters";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return mode == 0 ? "bassboost <level>" : mode == 1 ? "equalizer <band> <gain>" : "reset";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext ctx) {
|
||||
var m = FilterCommands.mgr(pm, ctx);
|
||||
if (!FilterCommands.checkChannel(m, ctx)) {
|
||||
return;
|
||||
}
|
||||
if (mode == 2) {
|
||||
m.getScheduler().resetFilters();
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Фильтры", "Все фильтры сброшены."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (mode == 1 ? ctx.getArgs().size() < 2 : ctx.getArgs().isEmpty()) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
mode == 1
|
||||
? (ctx.getArgs().isEmpty()
|
||||
? EmbedTemplates.info(
|
||||
"Equalizer",
|
||||
"Использование: `"
|
||||
+ getUsage()
|
||||
+ "`\nБанды: 0-14, усиление: -0.25 до 1.0")
|
||||
: EmbedTemplates.error("Аргументы", "Укажи band и gain"))
|
||||
: EmbedTemplates.error("Аргументы", "Укажи уровень от -25 до 25"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (mode == 1) {
|
||||
int band = Math.max(0, Math.min(14, Integer.parseInt(ctx.getArgs().get(0))));
|
||||
float gain =
|
||||
(float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(ctx.getArgs().get(1))));
|
||||
m.getScheduler().setEqualizerBand(band, gain);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info("Equalizer", "Band **" + band + "** → **" + gain + "**"))
|
||||
.queue();
|
||||
} else {
|
||||
int v = Math.max(-25, Math.min(25, Integer.parseInt(ctx.getArgs().get(0))));
|
||||
m.getScheduler().setBassboost(v);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Bassboost", "Установлен **" + v + "**"))
|
||||
.queue();
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Аргументы",
|
||||
mode == 1
|
||||
? "Нужны числа: band (0-14) и gain (-0.25 до 1.0)."
|
||||
: "Нужно число от -25 до 25."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
|
|
@ -30,11 +29,11 @@ public class FilterCommands {
|
|||
return new BandCommand(pm, 2);
|
||||
}
|
||||
|
||||
private static GuildMusicManager mgr(PlayerManager pm, final CommandContext ctx) {
|
||||
static GuildMusicManager mgr(PlayerManager pm, final CommandContext ctx) {
|
||||
return pm.getMusicManager(ctx.getGuild().getIdLong());
|
||||
}
|
||||
|
||||
private static boolean checkChannel(GuildMusicManager m, final CommandContext ctx) {
|
||||
static boolean checkChannel(GuildMusicManager m, final CommandContext ctx) {
|
||||
if (!m.getLink().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -43,145 +42,4 @@ public class FilterCommands {
|
|||
.queue();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class BandCommand implements Command {
|
||||
private final PlayerManager pm;
|
||||
private final int mode;
|
||||
|
||||
BandCommand(PlayerManager pm, int mode) {
|
||||
this.pm = pm;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return mode == 0 ? "bassboost" : mode == 1 ? "equalizer" : "reset";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return mode == 0
|
||||
? "Bassboost (-25 to 25)"
|
||||
: mode == 1 ? "Set equalizer band (0-14, gain -0.25 to 1.0)" : "Reset all filters";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return mode == 0 ? "bassboost <level>" : mode == 1 ? "equalizer <band> <gain>" : "reset";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext ctx) {
|
||||
var m = mgr(pm, ctx);
|
||||
if (!checkChannel(m, ctx)) {
|
||||
return;
|
||||
}
|
||||
if (mode == 2) {
|
||||
m.getScheduler().resetFilters();
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Фильтры", "Все фильтры сброшены."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (mode == 1 ? ctx.getArgs().size() < 2 : ctx.getArgs().isEmpty()) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
mode == 1
|
||||
? (ctx.getArgs().isEmpty()
|
||||
? EmbedTemplates.info(
|
||||
"Equalizer",
|
||||
"Использование: `"
|
||||
+ getUsage()
|
||||
+ "`\nБанды: 0-14, усиление: -0.25 до 1.0")
|
||||
: EmbedTemplates.error("Аргументы", "Укажи band и gain"))
|
||||
: EmbedTemplates.error("Аргументы", "Укажи уровень от -25 до 25"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (mode == 1) {
|
||||
int band = Math.max(0, Math.min(14, Integer.parseInt(ctx.getArgs().get(0))));
|
||||
float gain =
|
||||
(float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(ctx.getArgs().get(1))));
|
||||
m.getScheduler().setEqualizerBand(band, gain);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info("Equalizer", "Band **" + band + "** → **" + gain + "**"))
|
||||
.queue();
|
||||
} else {
|
||||
int v = Math.max(-25, Math.min(25, Integer.parseInt(ctx.getArgs().get(0))));
|
||||
m.getScheduler().setBassboost(v);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Bassboost", "Установлен **" + v + "**"))
|
||||
.queue();
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Аргументы",
|
||||
mode == 1
|
||||
? "Нужны числа: band (0-14) и gain (-0.25 до 1.0)."
|
||||
: "Нужно число от -25 до 25."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ToggleCommand implements Command {
|
||||
private final PlayerManager pm;
|
||||
private final boolean isNight;
|
||||
|
||||
ToggleCommand(PlayerManager pm, boolean isNight) {
|
||||
this.pm = pm;
|
||||
this.isNight = isNight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return isNight ? "nightcore" : "vaporwave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return isNight ? "Toggle nightcore effect" : "Toggle vaporwave effect";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return isNight ? "nightcore" : "vaporwave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext ctx) {
|
||||
var m = mgr(pm, ctx);
|
||||
if (!checkChannel(m, ctx)) {
|
||||
return;
|
||||
}
|
||||
boolean enabled =
|
||||
isNight
|
||||
? !m.getScheduler().filterState.isNightcore()
|
||||
: !m.getScheduler().filterState.isVaporwave();
|
||||
if (isNight) {
|
||||
m.getScheduler().setNightcore(enabled);
|
||||
} else {
|
||||
m.getScheduler().setVaporwave(enabled);
|
||||
}
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
isNight ? "Nightcore" : "Vaporwave", enabled ? "✅ Включён" : "❌ Выключен"))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.util.GuildAudioActions;
|
||||
|
||||
final class LeaveCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
LeaveCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "leave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Отключить бота от голосового канала";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "leave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var guild = context.getGuild();
|
||||
GuildAudioActions.stopAndDisconnect(playerManager.getMusicManager(guild.getIdLong()), guild);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Отключён", "Бот отключён от голосового канала."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
|
||||
public class PlayerCommands {
|
||||
|
||||
private PlayerCommands() {}
|
||||
|
||||
public static Command skip(PlayerManager playerManager) {
|
||||
|
|
@ -22,145 +17,4 @@ public class PlayerCommands {
|
|||
public static Command leave(PlayerManager playerManager) {
|
||||
return new LeaveCommand(playerManager);
|
||||
}
|
||||
|
||||
private static final class SkipCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
SkipCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "skip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Пропустить текущий трек";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "skip [количество]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var manager = getManager(context);
|
||||
if (manager.getScheduler().getCurrentTrack() == null) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
int amount = 1;
|
||||
if (!context.getArgs().isEmpty()) {
|
||||
try {
|
||||
amount = Integer.parseInt(context.getArgs().get(0));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
String skipped = manager.getScheduler().getCurrentTrack().getInfo().getTitle();
|
||||
for (int i = 0; i < amount - 1; i++) {
|
||||
manager.getScheduler().nextTrack();
|
||||
}
|
||||
manager.getScheduler().skip();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Пропущено", "**" + skipped + "**"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private GuildMusicManager getManager(final CommandContext context) {
|
||||
return playerManager.getMusicManager(context.getGuild().getIdLong());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StopCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
StopCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Остановить музыку и очистить очередь";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var guild = context.getGuild();
|
||||
var manager = playerManager.getMusicManager(guild.getIdLong());
|
||||
manager.stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LeaveCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
LeaveCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "leave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Отключить бота от голосового канала";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "leave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var guild = context.getGuild();
|
||||
var manager = playerManager.getMusicManager(guild.getIdLong());
|
||||
manager.stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Отключён", "Бот отключён от голосового канала."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,151 +3,75 @@ package creator.loki.feature.music.commands;
|
|||
import static creator.loki.embed.EmbedTemplates.error;
|
||||
import static creator.loki.embed.EmbedTemplates.info;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.util.QueueView;
|
||||
import creator.loki.feature.music.util.VolumeControl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QueueCommands {
|
||||
private static final Color C = new Color(88, 101, 242);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
|
||||
|
||||
private QueueCommands() {}
|
||||
|
||||
public static Command queue(final PlayerManager pm) {
|
||||
return new Mc(pm, "queue", "Показать очередь треков", null, null, QueueCommands::executeQueue);
|
||||
return new SimpleCommand(
|
||||
pm, "queue", "Показать очередь треков", null, null, QueueCommands::executeQueue);
|
||||
}
|
||||
|
||||
public static Command nowplaying(final PlayerManager pm) {
|
||||
BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeNowPlaying;
|
||||
return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec);
|
||||
return new SimpleCommand(
|
||||
pm,
|
||||
"nowplaying",
|
||||
"Показать текущий трек",
|
||||
"nowplaying",
|
||||
"Музыка",
|
||||
QueueCommands::executeNowPlaying);
|
||||
}
|
||||
|
||||
public static Command volume(final PlayerManager pm) {
|
||||
BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeVolume;
|
||||
return new Mc(pm, "volume", "Регулировка громкости (0-100)", "volume [0-100]", "Музыка", exec);
|
||||
}
|
||||
|
||||
private static final class Mc implements Command {
|
||||
private final PlayerManager p;
|
||||
private final String n;
|
||||
private final String d;
|
||||
private final String u;
|
||||
private final String c;
|
||||
private final BiConsumer<PlayerManager, CommandContext> e;
|
||||
|
||||
Mc(
|
||||
PlayerManager p,
|
||||
String n,
|
||||
String d,
|
||||
String u,
|
||||
String c,
|
||||
BiConsumer<PlayerManager, CommandContext> e) {
|
||||
this.p = p;
|
||||
this.n = n;
|
||||
this.d = d;
|
||||
this.u = u;
|
||||
this.c = c;
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return n;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return d;
|
||||
}
|
||||
|
||||
public String getUsage() {
|
||||
return u != null ? u : n;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return c != null ? c : "Общие";
|
||||
}
|
||||
|
||||
public void execute(final CommandContext ctx) {
|
||||
e.accept(p, ctx);
|
||||
}
|
||||
return new SimpleCommand(
|
||||
pm,
|
||||
"volume",
|
||||
"Регулировка громкости (0-100)",
|
||||
"volume [0-100]",
|
||||
"Музыка",
|
||||
QueueCommands::executeVolume);
|
||||
}
|
||||
|
||||
private static void executeQueue(final PlayerManager pm, final CommandContext ctx) {
|
||||
var m = pm.getMusicManager(ctx.getGuild().getIdLong());
|
||||
var cur = m.getScheduler().getCurrentTrack();
|
||||
var q = m.getScheduler().getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
var embed = QueueView.queue(pm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler());
|
||||
if (embed == null) {
|
||||
ctx.getChannel().sendMessage("Очередь пуста.").queue();
|
||||
return;
|
||||
}
|
||||
var embed = EmbedFactory.create().withTitle("Очередь треков").withColor(C);
|
||||
if (cur != null) {
|
||||
var i = cur.getInfo();
|
||||
embed.addField("Сейчас играет", "**" + i.getTitle() + "**\n" + i.getAuthor(), false);
|
||||
}
|
||||
if (!q.isEmpty()) {
|
||||
var sb = new StringBuilder();
|
||||
var show = Math.min(q.size(), 10);
|
||||
for (var i = 0; i < show; i++) {
|
||||
sb.append(String.format("`%d.` **%s**\n", i + 1, q.get(i).getInfo().getTitle()));
|
||||
}
|
||||
if (q.size() > show) {
|
||||
sb.append("... и ещё **").append(q.size() - show).append("** треков");
|
||||
}
|
||||
embed.addField("В очереди (" + q.size() + ")", sb.toString(), false);
|
||||
}
|
||||
ctx.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||
ctx.getChannel().sendMessageEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
private static void executeNowPlaying(final PlayerManager pm, final CommandContext ctx) {
|
||||
var lm = pm.getLavalinkManager();
|
||||
var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack();
|
||||
if (track == null) {
|
||||
var guildId = ctx.getGuild().getIdLong();
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(guildId);
|
||||
var embed = QueueView.nowPlaying(pm.getMusicManager(guildId).getScheduler(), link);
|
||||
if (embed == null) {
|
||||
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
|
||||
return;
|
||||
}
|
||||
var l = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong());
|
||||
var pos = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getPosition() : 0L;
|
||||
var dur = track.getInfo().getLength();
|
||||
var ds = dur / 1000 <= 0 ? 1 : dur / 1000;
|
||||
var filled = (int) ((double) pos / dur * 20);
|
||||
var bar = new StringBuilder("\u2503");
|
||||
for (var i = 0; i < 20; i++) {
|
||||
bar.append(i < filled ? "\u2588" : "\u2591");
|
||||
}
|
||||
var t0 = String.format("%d:%02d", pos / 1000 / 60, pos / 1000 % 60);
|
||||
var t1 = String.format("%d:%02d", ds / 60, ds % 60);
|
||||
var pbar = bar + "\u2503 " + t0 + " / " + t1;
|
||||
var msg =
|
||||
EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(C)
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField("Длительность", pbar, true)
|
||||
.build();
|
||||
ctx.getChannel().sendMessageEmbeds(msg).queue();
|
||||
ctx.getChannel().sendMessageEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
private static void executeVolume(final PlayerManager pm, final CommandContext ctx) {
|
||||
var l = pm.getLavalinkManager().getClient().getLinkIfCached(ctx.getGuild().getIdLong());
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ctx.getGuild().getIdLong());
|
||||
if (ctx.getArgs().isEmpty()) {
|
||||
var v = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getVolume() : 100;
|
||||
ctx.getChannel().sendMessageEmbeds(info("Громкость", "Текущая: **" + v + "%**")).queue();
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(info("Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0))));
|
||||
if (l != null && l.getCachedPlayer() != null) {
|
||||
l.createOrUpdatePlayer()
|
||||
.setVolume(vol)
|
||||
.subscribe(null, err -> LOG.warn("Lavalink: {}", err.getMessage()));
|
||||
}
|
||||
var vol = VolumeControl.clamp(Integer.parseInt(ctx.getArgs().get(0)));
|
||||
VolumeControl.set(link, vol, err -> LOG.warn("Lavalink: {}", err.getMessage()));
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**"))
|
||||
.queue();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
|
||||
final class SimpleCommand implements Command {
|
||||
private final PlayerManager p;
|
||||
private final String n;
|
||||
private final String d;
|
||||
private final String u;
|
||||
private final String c;
|
||||
private final BiConsumer<PlayerManager, CommandContext> e;
|
||||
|
||||
SimpleCommand(
|
||||
PlayerManager p,
|
||||
String n,
|
||||
String d,
|
||||
String u,
|
||||
String c,
|
||||
BiConsumer<PlayerManager, CommandContext> e) {
|
||||
this.p = p;
|
||||
this.n = n;
|
||||
this.d = d;
|
||||
this.u = u;
|
||||
this.c = c;
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return u != null ? u : n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return c != null ? c : "Общие";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext ctx) {
|
||||
e.accept(p, ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
|
||||
final class SkipCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
SkipCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "skip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Пропустить текущий трек";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "skip [количество]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var manager = getManager(context);
|
||||
if (manager.getScheduler().getCurrentTrack() == null) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
int amount = 1;
|
||||
if (!context.getArgs().isEmpty()) {
|
||||
try {
|
||||
amount = Integer.parseInt(context.getArgs().get(0));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
String skipped = manager.getScheduler().getCurrentTrack().getInfo().getTitle();
|
||||
for (int i = 0; i < amount - 1; i++) {
|
||||
manager.getScheduler().nextTrack();
|
||||
}
|
||||
manager.getScheduler().skip();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Пропущено", "**" + skipped + "**"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private GuildMusicManager getManager(final CommandContext context) {
|
||||
return playerManager.getMusicManager(context.getGuild().getIdLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.util.GuildAudioActions;
|
||||
|
||||
final class StopCommand implements Command {
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
StopCommand(PlayerManager playerManager) {
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Остановить музыку и очистить очередь";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
var guild = context.getGuild();
|
||||
GuildAudioActions.stopAndDisconnect(playerManager.getMusicManager(guild.getIdLong()), guild);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package creator.loki.feature.music.commands;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
|
||||
final class ToggleCommand implements Command {
|
||||
private final PlayerManager pm;
|
||||
private final boolean isNight;
|
||||
|
||||
ToggleCommand(PlayerManager pm, boolean isNight) {
|
||||
this.pm = pm;
|
||||
this.isNight = isNight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return isNight ? "nightcore" : "vaporwave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return isNight ? "Toggle nightcore effect" : "Toggle vaporwave effect";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return isNight ? "nightcore" : "vaporwave";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.MUSIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext ctx) {
|
||||
var m = FilterCommands.mgr(pm, ctx);
|
||||
if (!FilterCommands.checkChannel(m, ctx)) {
|
||||
return;
|
||||
}
|
||||
boolean enabled =
|
||||
isNight
|
||||
? !m.getScheduler().filterState.isNightcore()
|
||||
: !m.getScheduler().filterState.isVaporwave();
|
||||
if (isNight) {
|
||||
m.getScheduler().setNightcore(enabled);
|
||||
} else {
|
||||
m.getScheduler().setVaporwave(enabled);
|
||||
}
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
isNight ? "Nightcore" : "Vaporwave", enabled ? "✅ Включён" : "❌ Выключен"))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package creator.loki.feature.music.core;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.LavalinkClient;
|
||||
import dev.arbjerg.lavalink.client.event.ReadyEvent;
|
||||
import dev.arbjerg.lavalink.client.event.StatsEvent;
|
||||
import dev.arbjerg.lavalink.client.event.TrackEndEvent;
|
||||
import dev.arbjerg.lavalink.client.event.TrackStartEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class LavalinkEventRouter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LavalinkEventRouter.class);
|
||||
|
||||
private final LavalinkClient client;
|
||||
private final Map<Long, GuildMusicManager> musicManagers;
|
||||
|
||||
LavalinkEventRouter(LavalinkClient client, Map<Long, GuildMusicManager> musicManagers) {
|
||||
this.client = client;
|
||||
this.musicManagers = musicManagers;
|
||||
}
|
||||
|
||||
void register() {
|
||||
client
|
||||
.on(ReadyEvent.class)
|
||||
.subscribe(
|
||||
event ->
|
||||
LoggerDecorator.music(
|
||||
logger,
|
||||
"Нода '"
|
||||
+ event.getNode().getName()
|
||||
+ "' готова, sessionId: "
|
||||
+ event.getSessionId()));
|
||||
|
||||
client
|
||||
.on(StatsEvent.class)
|
||||
.subscribe(
|
||||
event ->
|
||||
logger.trace(
|
||||
"Нода '{}' статистика: игроков {}/{}",
|
||||
event.getNode().getName(),
|
||||
event.getPlayingPlayers(),
|
||||
event.getPlayers()));
|
||||
|
||||
client
|
||||
.on(TrackStartEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
GuildMusicManager manager = musicManagers.get(event.getGuildId());
|
||||
if (manager != null) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
}
|
||||
});
|
||||
|
||||
client
|
||||
.on(TrackEndEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
GuildMusicManager manager = musicManagers.get(event.getGuildId());
|
||||
if (manager != null) {
|
||||
manager.getScheduler().onTrackEnd(event.getTrack(), event.getEndReason());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -15,10 +15,6 @@ import dev.arbjerg.lavalink.client.Helpers;
|
|||
import dev.arbjerg.lavalink.client.LavalinkClient;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.NodeOptions;
|
||||
import dev.arbjerg.lavalink.client.event.ReadyEvent;
|
||||
import dev.arbjerg.lavalink.client.event.StatsEvent;
|
||||
import dev.arbjerg.lavalink.client.event.TrackEndEvent;
|
||||
import dev.arbjerg.lavalink.client.event.TrackStartEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -58,57 +54,12 @@ public class LavalinkManager {
|
|||
.setHttpTimeout(30000L)
|
||||
.build());
|
||||
|
||||
registerListeners();
|
||||
new LavalinkEventRouter(client, musicManagers).register();
|
||||
|
||||
LoggerDecorator.music(
|
||||
logger, "LavalinkManager инициализирован, нода: " + lavalinkConfig.getWsUri());
|
||||
}
|
||||
|
||||
private void registerListeners() {
|
||||
client
|
||||
.on(ReadyEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
LoggerDecorator.music(
|
||||
logger,
|
||||
"Нода '"
|
||||
+ event.getNode().getName()
|
||||
+ "' готова, sessionId: "
|
||||
+ event.getSessionId());
|
||||
});
|
||||
|
||||
client
|
||||
.on(StatsEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
logger.trace(
|
||||
"Нода '{}' статистика: игроков {}/{}",
|
||||
event.getNode().getName(),
|
||||
event.getPlayingPlayers(),
|
||||
event.getPlayers());
|
||||
});
|
||||
|
||||
client
|
||||
.on(TrackStartEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
GuildMusicManager manager = musicManagers.get(event.getGuildId());
|
||||
if (manager != null) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
}
|
||||
});
|
||||
|
||||
client
|
||||
.on(TrackEndEvent.class)
|
||||
.subscribe(
|
||||
event -> {
|
||||
GuildMusicManager manager = musicManagers.get(event.getGuildId());
|
||||
if (manager != null) {
|
||||
manager.getScheduler().onTrackEnd(event.getTrack(), event.getEndReason());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LavalinkClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package creator.loki.feature.music.listener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.play.TrackLoader;
|
||||
import creator.loki.feature.music.util.MusicEmbeds;
|
||||
import creator.loki.feature.music.util.TrackFormatter;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
|
||||
final class MessagePlayResponder implements TrackLoader.LoadCallbacks {
|
||||
private static final int GREEN = 0x00FF00;
|
||||
|
||||
private final MessageChannel ch;
|
||||
private final GuildMusicManager manager;
|
||||
private final String query;
|
||||
|
||||
MessagePlayResponder(MessageChannel ch, GuildMusicManager manager, String query) {
|
||||
this.ch = ch;
|
||||
this.manager = manager;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackLoaded(final Track track) {
|
||||
manager.getScheduler().enqueue(track);
|
||||
send(trackAdded(track));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(final PlaylistLoaded r) {
|
||||
for (Track t : r.getTracks()) {
|
||||
manager.getScheduler().enqueue(t);
|
||||
}
|
||||
send(playlistAdded(r));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResults(final List<Track> tracks) {
|
||||
if (tracks.isEmpty()) {
|
||||
onNoMatches();
|
||||
return;
|
||||
}
|
||||
Track t = tracks.get(0);
|
||||
manager.getScheduler().enqueue(t);
|
||||
send(trackAdded(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoMatches() {
|
||||
send(MusicEmbeds.err("Ничего не найдено: " + query));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(final LoadFailed e) {
|
||||
send(
|
||||
MusicEmbeds.err(
|
||||
"Ошибка: "
|
||||
+ (e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "неизвестная")));
|
||||
}
|
||||
|
||||
private EmbedBuilder trackAdded(final Track t) {
|
||||
var i = t.getInfo();
|
||||
return MusicEmbeds.ok("✅ Добавлено в очередь", "**[" + i.getTitle() + "](" + i.getUri() + ")**")
|
||||
.addField("Исполнитель", i.getAuthor(), true)
|
||||
.addField("Длительность", TrackFormatter.formatDuration(i.getLength()), true);
|
||||
}
|
||||
|
||||
private EmbedBuilder playlistAdded(final PlaylistLoaded pl) {
|
||||
long total = pl.getTracks().stream().mapToLong(t -> t.getInfo().getLength()).sum();
|
||||
return new EmbedBuilder()
|
||||
.setColor(GREEN)
|
||||
.setTitle("📀 Плейлист добавлен")
|
||||
.setDescription("**" + pl.getInfo().getName() + "**")
|
||||
.addField("Треков", String.valueOf(pl.getTracks().size()), true)
|
||||
.addField("Длительность", TrackFormatter.formatDuration(total), true);
|
||||
}
|
||||
|
||||
private void send(final EmbedBuilder eb) {
|
||||
ch.sendMessageEmbeds(eb.build()).queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package creator.loki.feature.music.listener;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.util.GuildAudioActions;
|
||||
import creator.loki.feature.music.util.MusicEmbeds;
|
||||
import creator.loki.feature.music.util.VoiceCheck;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -22,26 +25,25 @@ public class MusicBotControlHandler {
|
|||
}
|
||||
|
||||
public void handleSkip(final MessageChannel ch, final Guild guild, final Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
if (s.getCurrentTrack() == null) {
|
||||
send(ch, err("Нечего пропускать."));
|
||||
send(ch, MusicEmbeds.err("Нечего пропускать."));
|
||||
return;
|
||||
}
|
||||
var t = s.getCurrentTrack().getInfo().getTitle();
|
||||
s.skip();
|
||||
send(ch, ok("⏭ Пропущено", "**" + t + "**"));
|
||||
send(ch, MusicEmbeds.ok("⏭ Пропущено", "**" + t + "**"));
|
||||
}
|
||||
|
||||
public void handleStop(final MessageChannel ch, final Guild guild, final Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
send(ch, ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
||||
GuildAudioActions.stopAndDisconnect(lavalinkManager.getMusicManager(guild.getIdLong()), guild);
|
||||
send(ch, MusicEmbeds.ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
||||
}
|
||||
|
||||
public void handleQueue(final MessageChannel ch, final Guild guild) {
|
||||
|
|
@ -49,7 +51,7 @@ public class MusicBotControlHandler {
|
|||
var cur = s.getCurrentTrack();
|
||||
var q = s.getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
send(ch, err("Очередь пуста."));
|
||||
send(ch, MusicEmbeds.err("Очередь пуста."));
|
||||
return;
|
||||
}
|
||||
var sb = new StringBuilder();
|
||||
|
|
@ -69,31 +71,31 @@ public class MusicBotControlHandler {
|
|||
sb.append("... и ещё **").append(q.size() - show).append("** треков");
|
||||
}
|
||||
}
|
||||
send(ch, ok("📜 Очередь", sb.toString()));
|
||||
send(ch, MusicEmbeds.ok("📜 Очередь", sb.toString()));
|
||||
}
|
||||
|
||||
public void handleNowPlaying(final MessageChannel ch, final Guild guild) {
|
||||
var t = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().getCurrentTrack();
|
||||
if (t == null) {
|
||||
send(ch, err("Сейчас ничего не играет."));
|
||||
send(ch, MusicEmbeds.err("Сейчас ничего не играет."));
|
||||
return;
|
||||
}
|
||||
send(
|
||||
ch,
|
||||
ok("🎵 Сейчас играет", "**" + t.getInfo().getTitle() + "**\n" + t.getInfo().getAuthor()));
|
||||
MusicEmbeds.ok(
|
||||
"🎵 Сейчас играет", "**" + t.getInfo().getTitle() + "**\n" + t.getInfo().getAuthor()));
|
||||
}
|
||||
|
||||
public void handleLeave(final MessageChannel ch, final Guild guild, final Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
send(ch, ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
|
||||
GuildAudioActions.stopAndDisconnect(lavalinkManager.getMusicManager(guild.getIdLong()), guild);
|
||||
send(ch, MusicEmbeds.ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
|
||||
}
|
||||
|
||||
public void handleRepeat(final MessageChannel ch, final Guild guild, final Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
|
|
@ -101,45 +103,25 @@ public class MusicBotControlHandler {
|
|||
s.setRepeating(now);
|
||||
send(
|
||||
ch,
|
||||
ok(
|
||||
MusicEmbeds.ok(
|
||||
now ? "🔁 Повтор включён" : "➡ Повтор выключен",
|
||||
now ? "Очередь будет повторяться." : "Повтор отключён."));
|
||||
}
|
||||
|
||||
public void handleShuffle(final MessageChannel ch, final Guild guild, final Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
if (s.getQueue().isEmpty()) {
|
||||
send(ch, err("Очередь пуста."));
|
||||
send(ch, MusicEmbeds.err("Очередь пуста."));
|
||||
return;
|
||||
}
|
||||
s.shuffle();
|
||||
send(ch, ok("🔀 Перемешано", "Очередь перемешана."));
|
||||
}
|
||||
|
||||
private Member reqVoice(final MessageChannel ch, final Member member) {
|
||||
if (member != null && member.getVoiceState().inAudioChannel()) {
|
||||
return member;
|
||||
}
|
||||
send(ch, err("Зайди в голосовой канал сначала."));
|
||||
return null;
|
||||
send(ch, MusicEmbeds.ok("🔀 Перемешано", "Очередь перемешана."));
|
||||
}
|
||||
|
||||
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||
ch.sendMessageEmbeds(eb.build()).queue();
|
||||
}
|
||||
|
||||
private static EmbedBuilder ok(final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(0x00AAFF).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
private static EmbedBuilder ok(final int color, final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(color).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
private static EmbedBuilder err(final String msg) {
|
||||
return new EmbedBuilder().setColor(ERR).setDescription("❌ " + msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
package creator.loki.feature.music.listener;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -17,58 +13,15 @@ import org.jetbrains.annotations.NotNull;
|
|||
public class MusicBotListener extends ListenerAdapter {
|
||||
private static final String PREFIX = "m!";
|
||||
|
||||
private final MusicBotPlayHandler playHandler;
|
||||
private final MusicBotControlHandler controlHandler;
|
||||
private final Map<String, Consumer<MessageReceivedEvent>> commandMap;
|
||||
|
||||
public MusicBotListener(
|
||||
final LavalinkManager lavalinkManager,
|
||||
final QueueRepository queueRepository,
|
||||
final creator.loki.feature.music.core.PlayerManager playerManager) {
|
||||
this.playHandler = new MusicBotPlayHandler(lavalinkManager);
|
||||
this.controlHandler = new MusicBotControlHandler(lavalinkManager);
|
||||
this.commandMap = buildCommandMap();
|
||||
}
|
||||
|
||||
private Map<String, Consumer<MessageReceivedEvent>> buildCommandMap() {
|
||||
var map = new HashMap<String, Consumer<MessageReceivedEvent>>();
|
||||
map.put("play", ev -> playHandler.handlePlay(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put("skip", ev -> controlHandler.handleSkip(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("stop", ev -> controlHandler.handleStop(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("queue", ev -> controlHandler.handleQueue(ev.getChannel(), ev.getGuild()));
|
||||
map.put("nowplaying", ev -> controlHandler.handleNowPlaying(ev.getChannel(), ev.getGuild()));
|
||||
map.put("np", ev -> controlHandler.handleNowPlaying(ev.getChannel(), ev.getGuild()));
|
||||
map.put("volume", ev -> controlHandler.getFilterHandler()
|
||||
.handleVolume(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put("leave", ev -> controlHandler
|
||||
.handleLeave(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("bassboost", ev -> controlHandler.getFilterHandler()
|
||||
.handleBassboost(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put("nightcore", ev -> controlHandler.getFilterHandler()
|
||||
.handleNightcore(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("vaporwave", ev -> controlHandler.getFilterHandler()
|
||||
.handleVaporwave(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("reset", ev -> controlHandler.getFilterHandler()
|
||||
.handleReset(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("equalizer", ev -> controlHandler.getFilterHandler()
|
||||
.handleEqualizer(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put("repeat", ev -> controlHandler
|
||||
.handleRepeat(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("loop", ev -> controlHandler
|
||||
.handleRepeat(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("shuffle", ev -> controlHandler
|
||||
.handleShuffle(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("help", ev -> help(ev.getChannel()));
|
||||
map.put("commands", ev -> help(ev.getChannel()));
|
||||
return map;
|
||||
}
|
||||
|
||||
private static List<String> parseArgs(MessageReceivedEvent event) {
|
||||
String content = event.getMessage().getContentRaw();
|
||||
String[] parts = content.substring(PREFIX.length()).trim().split("\\s+");
|
||||
return parts.length > 1
|
||||
? List.of(java.util.Arrays.copyOfRange(parts, 1, parts.length))
|
||||
: List.of();
|
||||
final PlayerManager playerManager) {
|
||||
var playHandler = new MusicBotPlayHandler(lavalinkManager);
|
||||
var controlHandler = new MusicBotControlHandler(lavalinkManager);
|
||||
this.commandMap = MusicCommandMap.build(playHandler, controlHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -81,43 +34,13 @@ public class MusicBotListener extends ListenerAdapter {
|
|||
return;
|
||||
}
|
||||
String[] parts = content.substring(PREFIX.length()).trim().split("\\s+");
|
||||
if (parts.length == 0) {
|
||||
if (parts.length == 0 || event.getGuild() == null) {
|
||||
return;
|
||||
}
|
||||
String cmd = parts[0].toLowerCase();
|
||||
Guild guild = event.getGuild();
|
||||
if (guild == null) {
|
||||
return;
|
||||
}
|
||||
Consumer<MessageReceivedEvent> handler = commandMap.get(cmd);
|
||||
if (handler != null) {
|
||||
handler.accept(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void help(final MessageChannel ch) {
|
||||
ch.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00AAFF)
|
||||
.setTitle("🎵 Music Bot")
|
||||
.setDescription(
|
||||
"""
|
||||
`m!play <запрос/URL>` — Искать и играть
|
||||
`m!skip` — Пропустить
|
||||
`m!stop` — Остановить
|
||||
`m!queue` — Очередь
|
||||
`m!nowplaying` — Что играет
|
||||
`m!volume [0-100]` — Громкость
|
||||
`m!shuffle` — Перемешать
|
||||
`m!repeat` — Повтор
|
||||
`m!bassboost [-25..25]` — Басс
|
||||
`m!nightcore` — Nightcore ⚡
|
||||
`m!vaporwave` — Vaporwave 🌊
|
||||
`m!equalizer <band> <gain>` — Эквалайзер
|
||||
`m!reset` — Сбросить фильтры
|
||||
""")
|
||||
.setFooter("Jockie Music style", null)
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import java.util.List;
|
|||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.play.TrackLoader;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import creator.loki.feature.music.util.MusicEmbeds;
|
||||
import creator.loki.feature.music.util.TrackQuery;
|
||||
import creator.loki.feature.music.util.VoiceCheck;
|
||||
import creator.loki.feature.utility.VoiceHelper;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -24,158 +25,21 @@ public class MusicBotPlayHandler {
|
|||
|
||||
public void handlePlay(
|
||||
final MessageChannel ch, final Guild guild, Member member, final List<String> args) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
if (args.isEmpty()) {
|
||||
send(ch, err("Укажи URL или запрос: `m!play <название>`"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.startVirtualThread(() -> connect(guild, member, lavalinkManager)).join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
send(ch, MusicEmbeds.err("Укажи URL или запрос: `m!play <название>`"));
|
||||
return;
|
||||
}
|
||||
String query = String.join(" ", args);
|
||||
GuildMusicManager m = mgr(guild);
|
||||
String id = resolveIdentifier(query);
|
||||
|
||||
trackLoader.loadItem(
|
||||
guild.getIdLong(),
|
||||
id,
|
||||
new TrackLoader.LoadCallbacks() {
|
||||
@Override
|
||||
public void onTrackLoaded(final Track track) {
|
||||
m.getScheduler().enqueue(track);
|
||||
send(ch, trackAdded(track));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(final PlaylistLoaded r) {
|
||||
for (Track t : r.getTracks()) {
|
||||
m.getScheduler().enqueue(t);
|
||||
}
|
||||
send(ch, playlistAdded(r));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResults(final List<Track> tracks) {
|
||||
if (tracks.isEmpty()) {
|
||||
onNoMatches();
|
||||
return;
|
||||
}
|
||||
Track t = tracks.get(0);
|
||||
m.getScheduler().enqueue(t);
|
||||
send(ch, trackAdded(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoMatches() {
|
||||
send(ch, err("Ничего не найдено: " + query));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(final dev.arbjerg.lavalink.client.player.LoadFailed e) {
|
||||
send(
|
||||
ch,
|
||||
err(
|
||||
"Ошибка: "
|
||||
+ (e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "неизвестная")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Member reqVoice(final MessageChannel ch, Member member) {
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
send(ch, err("Зайди в голосовой канал сначала."));
|
||||
return null;
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
private GuildMusicManager mgr(final Guild g) {
|
||||
return lavalinkManager.getMusicManager(g.getIdLong());
|
||||
}
|
||||
|
||||
private static String resolveIdentifier(final String query) {
|
||||
if (query.startsWith("http://") || query.startsWith("https://")) {
|
||||
return query;
|
||||
}
|
||||
return "ytsearch:" + query;
|
||||
}
|
||||
|
||||
private void connect(final Guild g, final Member m, final LavalinkManager lm) {
|
||||
if (g.getMember(g.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) {
|
||||
return;
|
||||
}
|
||||
var vc = m.getVoiceState().getChannel().asVoiceChannel();
|
||||
g.getJDA().getDirectAudioController().connect(vc);
|
||||
|
||||
if (!g.getAudioManager().isConnected()) {
|
||||
g.getAudioManager().openAudioConnection(vc);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (g.getMember(g.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Link link = lm.getOrCreateLink(g.getIdLong());
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (link.getCachedPlayer() != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
VoiceHelper.connectForPlayback(guild, member, lavalinkManager);
|
||||
GuildMusicManager m = lavalinkManager.getMusicManager(guild.getIdLong());
|
||||
String id = TrackQuery.build(null, query);
|
||||
trackLoader.loadItem(guild.getIdLong(), id, new MessagePlayResponder(ch, m, query));
|
||||
}
|
||||
|
||||
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||
ch.sendMessageEmbeds(eb.build()).queue();
|
||||
}
|
||||
|
||||
private EmbedBuilder trackAdded(final Track t) {
|
||||
var i = t.getInfo();
|
||||
return ok("✅ Добавлено в очередь", "**[" + i.getTitle() + "](" + i.getUri() + ")**")
|
||||
.addField("Исполнитель", i.getAuthor(), true)
|
||||
.addField("Длительность", fmt(i.getLength()), true);
|
||||
}
|
||||
|
||||
private EmbedBuilder playlistAdded(final PlaylistLoaded pl) {
|
||||
long total = pl.getTracks().stream().mapToLong(t -> t.getInfo().getLength()).sum();
|
||||
return new EmbedBuilder()
|
||||
.setColor(0x00FF00)
|
||||
.setTitle("📀 Плейлист добавлен")
|
||||
.setDescription("**" + pl.getInfo().getName() + "**")
|
||||
.addField("Треков", String.valueOf(pl.getTracks().size()), true)
|
||||
.addField("Длительность", fmt(total), true);
|
||||
}
|
||||
|
||||
private static EmbedBuilder ok(final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(0x00AAFF).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
private static EmbedBuilder err(final String msg) {
|
||||
return new EmbedBuilder().setColor(0xFF4444).setDescription("❌ " + msg);
|
||||
}
|
||||
|
||||
static String fmt(final long ms) {
|
||||
long s = ms / 1000;
|
||||
if (s >= 3600) {
|
||||
return String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60);
|
||||
}
|
||||
return String.format("%d:%02d", s / 60, s % 60);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package creator.loki.feature.music.listener;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
|
||||
final class MusicCommandMap {
|
||||
private static final String PREFIX = "m!";
|
||||
|
||||
private MusicCommandMap() {}
|
||||
|
||||
static Map<String, Consumer<MessageReceivedEvent>> build(
|
||||
final MusicBotPlayHandler playHandler, final MusicBotControlHandler controlHandler) {
|
||||
var filters = controlHandler.getFilterHandler();
|
||||
var map = new HashMap<String, Consumer<MessageReceivedEvent>>();
|
||||
map.put(
|
||||
"play",
|
||||
ev ->
|
||||
playHandler.handlePlay(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put(
|
||||
"skip", ev -> controlHandler.handleSkip(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"stop", ev -> controlHandler.handleStop(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("queue", ev -> controlHandler.handleQueue(ev.getChannel(), ev.getGuild()));
|
||||
map.put("nowplaying", ev -> controlHandler.handleNowPlaying(ev.getChannel(), ev.getGuild()));
|
||||
map.put("np", ev -> controlHandler.handleNowPlaying(ev.getChannel(), ev.getGuild()));
|
||||
map.put(
|
||||
"volume",
|
||||
ev -> filters.handleVolume(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put(
|
||||
"leave", ev -> controlHandler.handleLeave(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"bassboost",
|
||||
ev ->
|
||||
filters.handleBassboost(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put(
|
||||
"nightcore", ev -> filters.handleNightcore(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"vaporwave", ev -> filters.handleVaporwave(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("reset", ev -> filters.handleReset(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"equalizer",
|
||||
ev ->
|
||||
filters.handleEqualizer(ev.getChannel(), ev.getGuild(), ev.getMember(), parseArgs(ev)));
|
||||
map.put(
|
||||
"repeat",
|
||||
ev -> controlHandler.handleRepeat(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"loop", ev -> controlHandler.handleRepeat(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put(
|
||||
"shuffle",
|
||||
ev -> controlHandler.handleShuffle(ev.getChannel(), ev.getGuild(), ev.getMember()));
|
||||
map.put("help", ev -> help(ev.getChannel()));
|
||||
map.put("commands", ev -> help(ev.getChannel()));
|
||||
return map;
|
||||
}
|
||||
|
||||
static List<String> parseArgs(final MessageReceivedEvent event) {
|
||||
String content = event.getMessage().getContentRaw();
|
||||
String[] parts = content.substring(PREFIX.length()).trim().split("\\s+");
|
||||
return parts.length > 1
|
||||
? List.of(java.util.Arrays.copyOfRange(parts, 1, parts.length))
|
||||
: List.of();
|
||||
}
|
||||
|
||||
private static void help(final MessageChannel ch) {
|
||||
ch.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00AAFF)
|
||||
.setTitle("🎵 Music Bot")
|
||||
.setDescription(
|
||||
"""
|
||||
`m!play <запрос/URL>` — Искать и играть
|
||||
`m!skip` — Пропустить
|
||||
`m!stop` — Остановить
|
||||
`m!queue` — Очередь
|
||||
`m!nowplaying` — Что играет
|
||||
`m!volume [0-100]` — Громкость
|
||||
`m!shuffle` — Перемешать
|
||||
`m!repeat` — Повтор
|
||||
`m!bassboost [-25..25]` — Басс
|
||||
`m!nightcore` — Nightcore ⚡
|
||||
`m!vaporwave` — Vaporwave 🌊
|
||||
`m!equalizer <band> <gain>` — Эквалайзер
|
||||
`m!reset` — Сбросить фильтры
|
||||
""")
|
||||
.setFooter("Jockie Music style", null)
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ package creator.loki.feature.music.listener;
|
|||
import java.util.List;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.util.MusicEmbeds;
|
||||
import creator.loki.feature.music.util.VoiceCheck;
|
||||
import creator.loki.feature.music.util.VolumeControl;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -12,7 +15,6 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
class MusicFilterHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(MusicFilterHandler.class);
|
||||
private static final int ERR = 0xFF4444;
|
||||
|
||||
private final LavalinkManager lavalinkManager;
|
||||
|
||||
|
|
@ -21,37 +23,25 @@ class MusicFilterHandler {
|
|||
}
|
||||
|
||||
void handleVolume(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong());
|
||||
if (args.isEmpty()) {
|
||||
send(
|
||||
ch,
|
||||
ok(
|
||||
"🔊 Громкость",
|
||||
"Текущая: **"
|
||||
+ (link != null && link.getCachedPlayer() != null
|
||||
? link.getCachedPlayer().getVolume()
|
||||
: 100)
|
||||
+ "%**"));
|
||||
send(ch, MusicEmbeds.ok("🔊 Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
if (link != null) {
|
||||
link.createOrUpdatePlayer()
|
||||
.setVolume(vol)
|
||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
|
||||
}
|
||||
send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**"));
|
||||
var vol = VolumeControl.clamp(Integer.parseInt(args.get(0)));
|
||||
VolumeControl.set(link, vol, err -> log.warn("Lavalink: {}", err.getMessage()));
|
||||
send(ch, MusicEmbeds.ok("🔊 Громкость", "Установлена **" + vol + "%**"));
|
||||
} catch (NumberFormatException e) {
|
||||
send(ch, err("Укажи число от 0 до 100."));
|
||||
send(ch, MusicEmbeds.err("Укажи число от 0 до 100."));
|
||||
}
|
||||
}
|
||||
|
||||
void handleBassboost(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var level = 10;
|
||||
|
|
@ -62,43 +52,43 @@ class MusicFilterHandler {
|
|||
}
|
||||
}
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level);
|
||||
send(ch, ok("🔊 Bassboost", "**" + level + "**"));
|
||||
send(ch, MusicEmbeds.ok("🔊 Bassboost", "**" + level + "**"));
|
||||
}
|
||||
|
||||
void handleNightcore(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
var now = !s.filterState.isNightcore();
|
||||
s.setNightcore(now);
|
||||
send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
|
||||
send(ch, MusicEmbeds.ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
|
||||
}
|
||||
|
||||
void handleVaporwave(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||
var now = !s.filterState.isVaporwave();
|
||||
s.setVaporwave(now);
|
||||
send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
|
||||
send(ch, MusicEmbeds.ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
|
||||
}
|
||||
|
||||
void handleReset(MessageChannel ch, Guild guild, Member member) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().resetFilters();
|
||||
send(ch, ok("🔄 Сброс", "Фильтры сброшены."));
|
||||
send(ch, MusicEmbeds.ok("🔄 Сброс", "Фильтры сброшены."));
|
||||
}
|
||||
|
||||
void handleEqualizer(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||
if (reqVoice(ch, member) == null) {
|
||||
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||
return;
|
||||
}
|
||||
if (args.size() < 2) {
|
||||
send(ch, err("Укажи band 0-14 и gain -0.25 до 1.0"));
|
||||
send(ch, MusicEmbeds.err("Укажи band 0-14 и gain -0.25 до 1.0"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -110,31 +100,14 @@ class MusicFilterHandler {
|
|||
.setEqualizerBand(band, gain);
|
||||
send(
|
||||
ch,
|
||||
ok(
|
||||
"🎛 Эквалайзер",
|
||||
"Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
|
||||
MusicEmbeds.ok(
|
||||
"🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
|
||||
} catch (NumberFormatException e) {
|
||||
send(ch, err("Неверный формат чисел."));
|
||||
send(ch, MusicEmbeds.err("Неверный формат чисел."));
|
||||
}
|
||||
}
|
||||
|
||||
private Member reqVoice(final MessageChannel ch, final Member member) {
|
||||
if (member != null && member.getVoiceState().inAudioChannel()) {
|
||||
return member;
|
||||
}
|
||||
send(ch, err("Зайди в голосовой канал сначала."));
|
||||
return null;
|
||||
}
|
||||
|
||||
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||
ch.sendMessageEmbeds(eb.build()).queue();
|
||||
}
|
||||
|
||||
private static EmbedBuilder ok(final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(0x00AAFF).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
private static EmbedBuilder err(final String msg) {
|
||||
return new EmbedBuilder().setColor(ERR).setDescription("❌ " + msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package creator.loki.feature.music.manage;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class AutoLeaveController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AutoLeaveController.class);
|
||||
private static final long AUTO_LEAVE_DELAY_MINUTES = 3;
|
||||
|
||||
private final GuildMusicManager manager;
|
||||
private final LongConsumer voiceDisconnectHandler;
|
||||
private ScheduledFuture<?> autoLeaveFuture;
|
||||
|
||||
AutoLeaveController(GuildMusicManager manager, LongConsumer voiceDisconnectHandler) {
|
||||
this.manager = manager;
|
||||
this.voiceDisconnectHandler = voiceDisconnectHandler;
|
||||
}
|
||||
|
||||
void start() {
|
||||
cancel();
|
||||
autoLeaveFuture =
|
||||
manager.getExecutor().schedule(this::autoLeave, AUTO_LEAVE_DELAY_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (autoLeaveFuture != null && !autoLeaveFuture.isDone()) {
|
||||
autoLeaveFuture.cancel(false);
|
||||
autoLeaveFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void autoLeave() {
|
||||
manager.stop();
|
||||
manager
|
||||
.getLink()
|
||||
.ifPresent(
|
||||
link ->
|
||||
link.destroy()
|
||||
.subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())));
|
||||
if (voiceDisconnectHandler != null) {
|
||||
voiceDisconnectHandler.accept(manager.getGuildId());
|
||||
}
|
||||
manager.getLavalinkManager().removeMusicManager(manager.getGuildId());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,23 @@
|
|||
package creator.loki.feature.music.manage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.feature.music.repository.QueueRepository.QueueTrack;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.LavalinkNode;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class GuildMusicManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuildMusicManager.class);
|
||||
private final long guildId;
|
||||
private final LavalinkManager lavalinkManager;
|
||||
private TrackScheduler scheduler;
|
||||
private final QueueRepository queueRepository;
|
||||
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
private ScheduledFuture<?> autoLeaveFuture;
|
||||
private final LongConsumer voiceDisconnectHandler;
|
||||
private final AutoLeaveController autoLeave;
|
||||
private final QueueStore queueStore;
|
||||
private TrackScheduler scheduler;
|
||||
|
||||
public GuildMusicManager(
|
||||
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
|
||||
|
|
@ -42,14 +31,14 @@ public class GuildMusicManager {
|
|||
LongConsumer voiceDisconnectHandler) {
|
||||
this.guildId = guildId;
|
||||
this.lavalinkManager = lavalinkManager;
|
||||
this.queueRepository = queueRepository;
|
||||
this.voiceDisconnectHandler = voiceDisconnectHandler;
|
||||
this.autoLeave = new AutoLeaveController(this, voiceDisconnectHandler);
|
||||
this.queueStore = new QueueStore(this, lavalinkManager, queueRepository);
|
||||
this.scheduler = null;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
this.scheduler = new TrackScheduler(this);
|
||||
loadQueueAsync();
|
||||
queueStore.loadAsync();
|
||||
}
|
||||
|
||||
public TrackScheduler getScheduler() {
|
||||
|
|
@ -65,7 +54,7 @@ public class GuildMusicManager {
|
|||
}
|
||||
|
||||
public void destroy() {
|
||||
cancelAutoLeaveTimer();
|
||||
autoLeave.cancel();
|
||||
scheduler.stop();
|
||||
executor.shutdown();
|
||||
try {
|
||||
|
|
@ -78,28 +67,11 @@ public class GuildMusicManager {
|
|||
}
|
||||
|
||||
public void startAutoLeaveTimer() {
|
||||
cancelAutoLeaveTimer();
|
||||
autoLeaveFuture = executor.schedule(this::autoLeave, 3, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void autoLeave() {
|
||||
stop();
|
||||
getLink()
|
||||
.ifPresent(
|
||||
link ->
|
||||
link.destroy()
|
||||
.subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())));
|
||||
if (voiceDisconnectHandler != null) {
|
||||
voiceDisconnectHandler.accept(guildId);
|
||||
}
|
||||
lavalinkManager.removeMusicManager(guildId);
|
||||
autoLeave.start();
|
||||
}
|
||||
|
||||
public void cancelAutoLeaveTimer() {
|
||||
if (autoLeaveFuture != null && !autoLeaveFuture.isDone()) {
|
||||
autoLeaveFuture.cancel(false);
|
||||
autoLeaveFuture = null;
|
||||
}
|
||||
autoLeave.cancel();
|
||||
}
|
||||
|
||||
public Optional<Link> getLink() {
|
||||
|
|
@ -118,77 +90,11 @@ public class GuildMusicManager {
|
|||
return guildId;
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private void loadQueueAsync() {
|
||||
if (queueRepository == null) {
|
||||
return;
|
||||
}
|
||||
var saved = queueRepository.loadQueue(String.valueOf(guildId));
|
||||
if (saved.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var client = lavalinkManager.getClient(); // NOPMD: shared client, not owned by this method
|
||||
if (client == null || client.getNodes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
executor.execute(() -> restoreTracks(saved, client.getNodes().get(0)));
|
||||
}
|
||||
|
||||
private void restoreTracks(final List<QueueTrack> saved, final LavalinkNode node) {
|
||||
var decoded = new ArrayList<Track>();
|
||||
for (var qt : saved) {
|
||||
try {
|
||||
var track = node.decodeTrack(qt.encoded()).block();
|
||||
if (track != null) {
|
||||
decoded.add(track);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title());
|
||||
}
|
||||
}
|
||||
if (decoded.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var q = scheduler.queue;
|
||||
q.addAll(decoded.subList(1, decoded.size()));
|
||||
var first = decoded.get(0);
|
||||
var player = getPlayer().orElse(null);
|
||||
if (player != null && player.getTrack() == null) {
|
||||
scheduler.startTrack(first);
|
||||
} else {
|
||||
q.offer(first);
|
||||
}
|
||||
LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
|
||||
queueRepository.deleteQueue(String.valueOf(guildId));
|
||||
}
|
||||
|
||||
private ScheduledFuture<?> saveDebounced;
|
||||
|
||||
void saveQueueDebounced() {
|
||||
if (saveDebounced != null && !saveDebounced.isDone()) {
|
||||
saveDebounced.cancel(false);
|
||||
}
|
||||
saveDebounced = executor.schedule(this::saveQueue, 3, TimeUnit.SECONDS);
|
||||
queueStore.saveDebounced();
|
||||
}
|
||||
|
||||
void saveQueue() {
|
||||
if (queueRepository == null) {
|
||||
return;
|
||||
}
|
||||
var current = scheduler.getCurrentTrack();
|
||||
var tracks = new ArrayList<>(scheduler.getQueue());
|
||||
var queueTracks = new ArrayList<QueueTrack>();
|
||||
if (current != null) {
|
||||
queueTracks.add(toQueueTrack(current));
|
||||
}
|
||||
for (var t : tracks) {
|
||||
queueTracks.add(toQueueTrack(t));
|
||||
}
|
||||
queueRepository.saveQueue(String.valueOf(guildId), queueTracks);
|
||||
}
|
||||
|
||||
private static QueueTrack toQueueTrack(final Track t) {
|
||||
var i = t.getInfo();
|
||||
return new QueueTrack(t.getEncoded(), i.getTitle(), i.getAuthor(), i.getUri(), i.getLength());
|
||||
queueStore.save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
105
src/main/java/creator/loki/feature/music/manage/QueueStore.java
Normal file
105
src/main/java/creator/loki/feature/music/manage/QueueStore.java
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package creator.loki.feature.music.manage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.feature.music.repository.QueueRepository.QueueTrack;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.LavalinkNode;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class QueueStore {
|
||||
private static final Logger logger = LoggerFactory.getLogger(QueueStore.class);
|
||||
private static final long SAVE_DELAY_SECONDS = 3;
|
||||
|
||||
private final GuildMusicManager manager;
|
||||
private final LavalinkManager lavalinkManager;
|
||||
private final QueueRepository queueRepository;
|
||||
private ScheduledFuture<?> saveDebounced;
|
||||
|
||||
QueueStore(
|
||||
GuildMusicManager manager, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
|
||||
this.manager = manager;
|
||||
this.lavalinkManager = lavalinkManager;
|
||||
this.queueRepository = queueRepository;
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
void loadAsync() {
|
||||
if (queueRepository == null) {
|
||||
return;
|
||||
}
|
||||
var saved = queueRepository.loadQueue(String.valueOf(manager.getGuildId()));
|
||||
if (saved.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var client = lavalinkManager.getClient(); // NOPMD: shared client, not owned by this method
|
||||
if (client == null || client.getNodes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
manager.getExecutor().execute(() -> restoreTracks(saved, client.getNodes().get(0)));
|
||||
}
|
||||
|
||||
private void restoreTracks(final List<QueueTrack> saved, final LavalinkNode node) {
|
||||
var decoded = new ArrayList<Track>();
|
||||
for (var qt : saved) {
|
||||
try {
|
||||
var track = node.decodeTrack(qt.encoded()).block();
|
||||
if (track != null) {
|
||||
decoded.add(track);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title());
|
||||
}
|
||||
}
|
||||
if (decoded.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var q = manager.getScheduler().queue;
|
||||
q.addAll(decoded.subList(1, decoded.size()));
|
||||
var first = decoded.get(0);
|
||||
var player = manager.getPlayer().orElse(null);
|
||||
if (player != null && player.getTrack() == null) {
|
||||
manager.getScheduler().startTrack(first);
|
||||
} else {
|
||||
q.offer(first);
|
||||
}
|
||||
LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков");
|
||||
queueRepository.deleteQueue(String.valueOf(manager.getGuildId()));
|
||||
}
|
||||
|
||||
void saveDebounced() {
|
||||
if (saveDebounced != null && !saveDebounced.isDone()) {
|
||||
saveDebounced.cancel(false);
|
||||
}
|
||||
saveDebounced =
|
||||
manager.getExecutor().schedule(this::save, SAVE_DELAY_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (queueRepository == null) {
|
||||
return;
|
||||
}
|
||||
var current = manager.getScheduler().getCurrentTrack();
|
||||
var tracks = new ArrayList<>(manager.getScheduler().getQueue());
|
||||
var queueTracks = new ArrayList<QueueTrack>();
|
||||
if (current != null) {
|
||||
queueTracks.add(toQueueTrack(current));
|
||||
}
|
||||
for (var t : tracks) {
|
||||
queueTracks.add(toQueueTrack(t));
|
||||
}
|
||||
queueRepository.saveQueue(String.valueOf(manager.getGuildId()), queueTracks);
|
||||
}
|
||||
|
||||
private static QueueTrack toQueueTrack(final Track t) {
|
||||
var i = t.getInfo();
|
||||
return new QueueTrack(t.getEncoded(), i.getTitle(), i.getAuthor(), i.getUri(), i.getLength());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package creator.loki.feature.music.manage;
|
||||
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class TrackEndHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(TrackEndHandler.class);
|
||||
|
||||
private final TrackScheduler scheduler;
|
||||
private final GuildMusicManager manager;
|
||||
|
||||
TrackEndHandler(TrackScheduler scheduler, GuildMusicManager manager) {
|
||||
this.scheduler = scheduler;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
void onTrackEnd(final Track lastTrack, final AudioTrackEndReason endReason) {
|
||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
||||
handleLoadFailed();
|
||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||
handleUnexpectedEnd(endReason);
|
||||
} else if (scheduler.isRepeating() && endReason == AudioTrackEndReason.FINISHED) {
|
||||
handleFinished(lastTrack);
|
||||
} else {
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLoadFailed() {
|
||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||
scheduler.notifyLoadFailed();
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
|
||||
private void handleUnexpectedEnd(final AudioTrackEndReason endReason) {
|
||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
|
||||
private void handleFinished(final Track lastTrack) {
|
||||
scheduler.startTrack(lastTrack.makeClone());
|
||||
}
|
||||
|
||||
private void nextOrAutoLeave() {
|
||||
var next = scheduler.pollNext();
|
||||
if (next != null) {
|
||||
scheduler.startTrack(next);
|
||||
} else {
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import java.util.List;
|
|||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
||||
|
|
@ -17,8 +16,10 @@ import org.slf4j.LoggerFactory;
|
|||
public class TrackScheduler {
|
||||
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
||||
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
||||
|
||||
private final GuildMusicManager manager;
|
||||
private final TrackStarter trackStarter;
|
||||
private final TrackEndHandler trackEndHandler;
|
||||
final Queue<Track> queue = new LinkedList<>();
|
||||
private boolean repeating;
|
||||
public final FilterState filterState = new FilterState();
|
||||
|
|
@ -27,6 +28,7 @@ public class TrackScheduler {
|
|||
public TrackScheduler(GuildMusicManager manager) {
|
||||
this.manager = manager;
|
||||
this.trackStarter = new TrackStarter(manager.getExecutor());
|
||||
this.trackEndHandler = new TrackEndHandler(this, manager);
|
||||
}
|
||||
|
||||
public void setOnLoadFailedNotify(Consumer<String> onLoadFailedNotify) {
|
||||
|
|
@ -109,15 +111,7 @@ public class TrackScheduler {
|
|||
}
|
||||
|
||||
public void onTrackEnd(final Track lastTrack, AudioTrackEndReason endReason) {
|
||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
||||
handleLoadFailed();
|
||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||
handleUnexpectedEnd(endReason);
|
||||
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
||||
handleFinished(lastTrack);
|
||||
} else {
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
trackEndHandler.onTrackEnd(lastTrack, endReason);
|
||||
}
|
||||
|
||||
void startTrack(final Track track) {
|
||||
|
|
@ -126,29 +120,13 @@ public class TrackScheduler {
|
|||
trackStarter.startWithRetry(link, track);
|
||||
}
|
||||
|
||||
private void handleLoadFailed() {
|
||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||
void notifyLoadFailed() {
|
||||
if (onLoadFailedNotify != null) {
|
||||
onLoadFailedNotify.accept("⚠️ Ошибка загрузки трека, пропускаю...");
|
||||
}
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
|
||||
private void handleUnexpectedEnd(final AudioTrackEndReason endReason) {
|
||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
|
||||
private void handleFinished(final Track lastTrack) {
|
||||
startTrack(lastTrack.makeClone());
|
||||
}
|
||||
|
||||
private void nextOrAutoLeave() {
|
||||
var next = queue.poll();
|
||||
if (next != null) {
|
||||
startTrack(next);
|
||||
} else {
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
Track pollNext() {
|
||||
return queue.poll();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import creator.loki.feature.music.core.LavalinkManager;
|
|||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.core.SearchManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.util.QueueNotifier;
|
||||
import creator.loki.feature.music.util.TrackQuery;
|
||||
import creator.loki.feature.utility.VoiceHelper;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
|
@ -18,6 +20,7 @@ import net.dv8tion.jda.api.entities.Member;
|
|||
|
||||
public class PlayCommand implements Command {
|
||||
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
|
||||
|
||||
private final PlayerManager playerManager;
|
||||
private final SearchHandler searchHandler;
|
||||
|
||||
|
|
@ -67,23 +70,12 @@ public class PlayCommand implements Command {
|
|||
handlePickResult(ctx, first);
|
||||
return;
|
||||
}
|
||||
if (!resolveVoiceConnection(ctx)) {
|
||||
return;
|
||||
}
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
var mm = lm.getMusicManager(guild.getIdLong());
|
||||
handlePlayQuery(ctx, args, first, lm, mm);
|
||||
}
|
||||
|
||||
private boolean resolveVoiceConnection(final CommandContext ctx) {
|
||||
var guild = ctx.getGuild();
|
||||
var member = ctx.getAuthor();
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
if (!VoiceHelper.connectForPlayback(guild, member, lm)) {
|
||||
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
handlePlayQuery(ctx, args, first, lm, lm.getMusicManager(guild.getIdLong()));
|
||||
}
|
||||
|
||||
private void handlePickResult(final CommandContext ctx, final String first) {
|
||||
|
|
@ -93,10 +85,15 @@ public class PlayCommand implements Command {
|
|||
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
||||
return;
|
||||
}
|
||||
if (!resolveVoiceConnection(ctx)) {
|
||||
if (!VoiceHelper.connectForPlayback(
|
||||
guild, ctx.getAuthor(), playerManager.getLavalinkManager())) {
|
||||
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
||||
return;
|
||||
}
|
||||
enqueueAndNotify(ctx, t);
|
||||
QueueNotifier.enqueueAndNotify(
|
||||
playerManager.getLavalinkManager().getMusicManager(guild.getIdLong()).getScheduler(),
|
||||
ctx.getChannel(),
|
||||
t);
|
||||
}
|
||||
|
||||
private void handlePlayQuery(
|
||||
|
|
@ -111,53 +108,11 @@ public class PlayCommand implements Command {
|
|||
sendErr(ctx, "Укажи запрос после источника.");
|
||||
return;
|
||||
}
|
||||
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
||||
String query = isUrl ? null : raw;
|
||||
String id = buildQuery(first, raw);
|
||||
loadAndHandle(
|
||||
new PlayContext(ctx, lm, ctx.getGuild().getIdLong(), mm),
|
||||
new PlayQuery(id, isUrl, query, raw));
|
||||
}
|
||||
|
||||
private record PlayContext(
|
||||
CommandContext ctx, LavalinkManager lm, long guildId, GuildMusicManager mm) {}
|
||||
|
||||
private record PlayQuery(String id, boolean isUrl, String query, String raw) {}
|
||||
|
||||
private void loadAndHandle(final PlayContext pc, final PlayQuery pq) {
|
||||
pc.lm()
|
||||
.getOrCreateLink(pc.guildId())
|
||||
.loadItem(pq.id())
|
||||
.subscribe(new PlayLoadHandler(pc.ctx(), searchHandler, pc.mm(), pq.isUrl(), pq.query(), pq.raw()));
|
||||
}
|
||||
|
||||
private static String buildQuery(final String first, final String raw) {
|
||||
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
||||
return raw;
|
||||
}
|
||||
if (raw.startsWith("ytsearch:")
|
||||
|| raw.startsWith("ytmsearch:")
|
||||
|| raw.startsWith("scsearch:")) {
|
||||
return raw;
|
||||
}
|
||||
if ("sc".equals(first)) {
|
||||
return "scsearch:" + raw;
|
||||
}
|
||||
return "ytsearch:" + raw;
|
||||
}
|
||||
|
||||
private void enqueueAndNotify(final CommandContext ctx, final Track track) {
|
||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||
m.getScheduler().enqueue(track);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Добавлено в очередь",
|
||||
"**"
|
||||
+ track.getInfo().getTitle()
|
||||
+ "**\n"
|
||||
+ SearchHandler.formatDuration(track.getInfo().getLength())))
|
||||
.queue();
|
||||
boolean isUrl = TrackQuery.isUrl(raw);
|
||||
String id = TrackQuery.build(first, raw);
|
||||
lm.getOrCreateLink(ctx.getGuild().getIdLong())
|
||||
.loadItem(id)
|
||||
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, isUrl ? null : raw, raw));
|
||||
}
|
||||
|
||||
private static void sendErr(final CommandContext ctx, final String msg) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package creator.loki.feature.music.play;
|
|||
import java.util.List;
|
||||
|
||||
import creator.loki.feature.music.core.SearchManager;
|
||||
import creator.loki.feature.music.util.TrackFormatter;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
|
||||
|
|
@ -68,13 +69,6 @@ public class SearchHandler {
|
|||
}
|
||||
|
||||
public static String formatDuration(final long millis) {
|
||||
long totalSec = millis / 1000;
|
||||
long hours = totalSec / 3600;
|
||||
long minutes = (totalSec % 3600) / 60;
|
||||
long secs = totalSec % 60;
|
||||
if (hours > 0) {
|
||||
return String.format("%d:%02d:%02d", hours, minutes, secs);
|
||||
}
|
||||
return String.format("%d:%02d", minutes, secs);
|
||||
return TrackFormatter.formatDuration(millis);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
package creator.loki.feature.music.slash;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.play.TrackLoader;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import creator.loki.feature.music.util.TrackQuery;
|
||||
import creator.loki.feature.music.util.VoiceCheck;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
|
||||
public class PlaySlashHandler {
|
||||
|
|
@ -23,138 +18,24 @@ public class PlaySlashHandler {
|
|||
}
|
||||
|
||||
public void handle(final SlashCommandInteractionEvent event) {
|
||||
Member member = requireVoiceChannel(event);
|
||||
if (member == null) {
|
||||
if (VoiceCheck.requireChannel(event) == null) {
|
||||
return;
|
||||
}
|
||||
String query = event.getOption("query").getAsString();
|
||||
Guild guild = event.getGuild();
|
||||
event.deferReply().queue();
|
||||
if (!guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) {
|
||||
if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
|
||||
guild
|
||||
.getJDA()
|
||||
.getDirectAudioController()
|
||||
.connect(member.getVoiceState().getChannel().asVoiceChannel());
|
||||
.connect(event.getMember().getVoiceState().getChannel().asVoiceChannel());
|
||||
}
|
||||
GuildMusicManager m = playerManager.getMusicManager(guild.getIdLong());
|
||||
if (query.startsWith("http://") || query.startsWith("https://")) {
|
||||
loadUrl(event, query, m, guild);
|
||||
} else {
|
||||
search(event, query, m, guild);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadUrl(
|
||||
final SlashCommandInteractionEvent event,
|
||||
final String url,
|
||||
final GuildMusicManager m,
|
||||
final Guild guild) {
|
||||
trackLoader.loadItem(m.getGuildId(), url, loader(event, m, guild, null));
|
||||
}
|
||||
|
||||
private void search(
|
||||
final SlashCommandInteractionEvent event,
|
||||
final String query,
|
||||
final GuildMusicManager m,
|
||||
final Guild guild) {
|
||||
trackLoader.loadItem(m.getGuildId(), "ytsearch:" + query, loader(event, m, guild, query));
|
||||
}
|
||||
|
||||
private TrackLoader.LoadCallbacks loader(
|
||||
final SlashCommandInteractionEvent event,
|
||||
final GuildMusicManager m,
|
||||
final Guild guild,
|
||||
final String searchQuery) {
|
||||
return new TrackLoader.LoadCallbacks() {
|
||||
@Override
|
||||
public void onTrackLoaded(final Track t) {
|
||||
addAndReply(event, m, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(final PlaylistLoaded r) {
|
||||
int c = 0;
|
||||
for (Track t : r.getTracks()) {
|
||||
m.getScheduler().enqueue(t);
|
||||
c++;
|
||||
}
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + c + " треков"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResults(final List<Track> ts) {
|
||||
if (ts.isEmpty()) {
|
||||
onNoMatches();
|
||||
return;
|
||||
}
|
||||
addAndReply(event, m, ts.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoMatches() {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Не найдено",
|
||||
searchQuery != null
|
||||
? "Ничего не найдено по запросу: " + searchQuery
|
||||
: "Ничего не найдено по ссылке."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(final LoadFailed e) {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка загрузки",
|
||||
e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "Неизвестная ошибка"))
|
||||
.queue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void addAndReply(
|
||||
final SlashCommandInteractionEvent event, final GuildMusicManager m, final Track t) {
|
||||
m.getScheduler().enqueue(t);
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Добавлено в очередь",
|
||||
"**" + t.getInfo().getTitle() + "**\n" + formatDuration(t.getInfo().getLength())))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private static Member requireVoiceChannel(final SlashCommandInteractionEvent event) {
|
||||
Member member = event.getMember();
|
||||
if (member != null && member.getVoiceState().inAudioChannel()) {
|
||||
return member;
|
||||
}
|
||||
event
|
||||
.replyEmbeds(EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return null;
|
||||
}
|
||||
|
||||
static String formatDuration(final long millis) {
|
||||
long totalSec = millis / 1000;
|
||||
long hours = totalSec / 3600;
|
||||
long minutes = (totalSec % 3600) / 60;
|
||||
long secs = totalSec % 60;
|
||||
if (hours > 0) {
|
||||
return String.format("%d:%02d:%02d", hours, minutes, secs);
|
||||
}
|
||||
return String.format("%d:%02d", minutes, secs);
|
||||
boolean isUrl = TrackQuery.isUrl(query);
|
||||
String searchQuery = isUrl ? null : query;
|
||||
trackLoader.loadItem(
|
||||
m.getGuildId(),
|
||||
TrackQuery.build(null, query),
|
||||
new SlashPlayResponder(event, m, searchQuery));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
package creator.loki.feature.music.slash;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import creator.loki.feature.music.util.GuildAudioActions;
|
||||
import creator.loki.feature.music.util.QueueView;
|
||||
import creator.loki.feature.music.util.VoiceCheck;
|
||||
import creator.loki.feature.music.util.VolumeControl;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -20,17 +19,17 @@ public class QueueSlashHandler {
|
|||
}
|
||||
|
||||
public void handleStop(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
pm.getMusicManager(ev.getGuild().getIdLong()).stop();
|
||||
ev.getGuild().getJDA().getDirectAudioController().disconnect(ev.getGuild());
|
||||
GuildAudioActions.stopAndDisconnect(
|
||||
pm.getMusicManager(ev.getGuild().getIdLong()), ev.getGuild());
|
||||
ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void handleSkip(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
|
|
@ -45,77 +44,44 @@ public class QueueSlashHandler {
|
|||
|
||||
public void handleQueue(final SlashCommandInteractionEvent ev) {
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
var cur = s.getCurrentTrack();
|
||||
var q = s.getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
var embed = QueueView.queue(s);
|
||||
if (embed == null) {
|
||||
ev.reply("Очередь пуста.").queue();
|
||||
return;
|
||||
}
|
||||
var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
|
||||
if (cur != null) {
|
||||
var ci = cur.getInfo();
|
||||
eb.addField("Сейчас играет", "**" + ci.getTitle() + "**\n" + ci.getAuthor(), false);
|
||||
}
|
||||
if (!q.isEmpty()) {
|
||||
eb.addField("В очереди (" + q.size() + ")", buildQueueList(q), false);
|
||||
}
|
||||
ev.replyEmbeds(eb.build()).queue();
|
||||
}
|
||||
|
||||
private String buildQueueList(final List<Track> q) {
|
||||
var sb = new StringBuilder();
|
||||
var show = Math.min(q.size(), 10);
|
||||
for (int i = 0; i < show; i++) {
|
||||
var t = q.get(i);
|
||||
sb.append("`").append(i + 1).append(".` **").append(t.getInfo().getTitle()).append("**\n");
|
||||
}
|
||||
if (q.size() > show) {
|
||||
sb.append("... и ещё **").append(q.size() - show).append("** треков");
|
||||
}
|
||||
return sb.toString();
|
||||
ev.replyEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
public void handleNowPlaying(final SlashCommandInteractionEvent ev) {
|
||||
var track = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().getCurrentTrack();
|
||||
if (track == null) {
|
||||
var guildId = ev.getGuild().getIdLong();
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(guildId);
|
||||
var embed = QueueView.nowPlaying(pm.getMusicManager(guildId).getScheduler(), link);
|
||||
if (embed == null) {
|
||||
ev.reply("Сейчас ничего не играет.").queue();
|
||||
return;
|
||||
}
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
||||
var pos =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0L;
|
||||
var eb =
|
||||
EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(new Color(88, 101, 242))
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField("Длительность", progressBar(pos, track.getInfo().getLength()), true)
|
||||
.build();
|
||||
ev.replyEmbeds(eb).queue();
|
||||
ev.replyEmbeds(embed).queue();
|
||||
}
|
||||
|
||||
public void handleVolume(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
||||
var opt = ev.getOption("percent");
|
||||
if (opt == null) {
|
||||
var cur =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100;
|
||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + cur + "%**")).queue();
|
||||
ev.replyEmbeds(
|
||||
EmbedTemplates.info("Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
var vol = Math.max(0, Math.min(100, opt.getAsInt()));
|
||||
if (link != null && link.getCachedPlayer() != null) {
|
||||
link.createOrUpdatePlayer().setVolume(vol).subscribe(null, LOG);
|
||||
}
|
||||
var vol = VolumeControl.clamp(opt.getAsInt());
|
||||
VolumeControl.set(link, vol, LOG);
|
||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
|
||||
}
|
||||
|
||||
public void handlePause(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||
|
|
@ -125,7 +91,7 @@ public class QueueSlashHandler {
|
|||
}
|
||||
|
||||
public void handleResume(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||
|
|
@ -135,7 +101,7 @@ public class QueueSlashHandler {
|
|||
}
|
||||
|
||||
public void handleShuffle(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle();
|
||||
|
|
@ -143,7 +109,7 @@ public class QueueSlashHandler {
|
|||
}
|
||||
|
||||
public void handleRepeat(final SlashCommandInteractionEvent ev) {
|
||||
if (reqVoice(ev) == null) {
|
||||
if (VoiceCheck.requireChannel(ev) == null) {
|
||||
return;
|
||||
}
|
||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||
|
|
@ -153,29 +119,6 @@ public class QueueSlashHandler {
|
|||
ev.replyEmbeds(EmbedTemplates.info("Повтор", d)).queue();
|
||||
}
|
||||
|
||||
private Member reqVoice(final SlashCommandInteractionEvent ev) {
|
||||
var m = ev.getMember();
|
||||
if (m != null && m.getVoiceState().inAudioChannel()) {
|
||||
return m;
|
||||
}
|
||||
var em = EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала.");
|
||||
ev.replyEmbeds(em).setEphemeral(true).queue();
|
||||
return null;
|
||||
}
|
||||
|
||||
static String progressBar(final long pos, final long duration) {
|
||||
long dur = duration <= 0 ? 1 : duration;
|
||||
int total = 20;
|
||||
int filled = (int) ((double) pos / dur * total);
|
||||
var bar = new StringBuilder("\u2503");
|
||||
for (int i = 0; i < total; i++) {
|
||||
bar.append(i < filled ? "\u2588" : "\u2591");
|
||||
}
|
||||
var p = String.format("%d:%02d", pos / 60000, (pos / 1000) % 60);
|
||||
var d = String.format("%d:%02d", dur / 60000, (dur / 1000) % 60);
|
||||
return bar + "\u2503 " + p + " / " + d;
|
||||
}
|
||||
|
||||
private static final Consumer<Throwable> LOG =
|
||||
err ->
|
||||
LoggerFactory.getLogger(QueueSlashHandler.class).warn("Lavalink: {}", err.getMessage());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package creator.loki.feature.music.slash;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.play.TrackLoader;
|
||||
import creator.loki.feature.music.util.TrackFormatter;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
|
||||
final class SlashPlayResponder implements TrackLoader.LoadCallbacks {
|
||||
private final SlashCommandInteractionEvent event;
|
||||
private final GuildMusicManager manager;
|
||||
private final String searchQuery;
|
||||
|
||||
SlashPlayResponder(
|
||||
SlashCommandInteractionEvent event, GuildMusicManager manager, String searchQuery) {
|
||||
this.event = event;
|
||||
this.manager = manager;
|
||||
this.searchQuery = searchQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackLoaded(final Track t) {
|
||||
addAndReply(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(final PlaylistLoaded r) {
|
||||
int count = 0;
|
||||
for (Track t : r.getTracks()) {
|
||||
manager.getScheduler().enqueue(t);
|
||||
count++;
|
||||
}
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + count + " треков"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResults(final List<Track> ts) {
|
||||
if (ts.isEmpty()) {
|
||||
onNoMatches();
|
||||
return;
|
||||
}
|
||||
addAndReply(ts.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoMatches() {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Не найдено",
|
||||
searchQuery != null
|
||||
? "Ничего не найдено по запросу: " + searchQuery
|
||||
: "Ничего не найдено по ссылке."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(final LoadFailed e) {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка загрузки",
|
||||
e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "Неизвестная ошибка"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void addAndReply(final Track t) {
|
||||
manager.getScheduler().enqueue(t);
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Добавлено в очередь",
|
||||
"**"
|
||||
+ t.getInfo().getTitle()
|
||||
+ "**\n"
|
||||
+ TrackFormatter.formatDuration(t.getInfo().getLength())))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
public final class GuildAudioActions {
|
||||
private GuildAudioActions() {}
|
||||
|
||||
public static void stopAndDisconnect(final GuildMusicManager manager, final Guild guild) {
|
||||
manager.stop();
|
||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
|
||||
public final class MusicEmbeds {
|
||||
private static final int ACCENT = 0x00AAFF;
|
||||
private static final int ERROR = 0xFF4444;
|
||||
|
||||
private MusicEmbeds() {}
|
||||
|
||||
public static EmbedBuilder ok(final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(ACCENT).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
public static EmbedBuilder ok(final int color, final String title, final String desc) {
|
||||
return new EmbedBuilder().setColor(color).setTitle(title).setDescription(desc);
|
||||
}
|
||||
|
||||
public static EmbedBuilder err(final String msg) {
|
||||
return new EmbedBuilder().setColor(ERROR).setDescription("❌ " + msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.manage.TrackScheduler;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
|
||||
public final class QueueNotifier {
|
||||
private QueueNotifier() {}
|
||||
|
||||
public static void enqueueAndNotify(
|
||||
final TrackScheduler scheduler, final MessageChannel ch, final Track t) {
|
||||
scheduler.enqueue(t);
|
||||
ch.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Добавлено в очередь",
|
||||
"**"
|
||||
+ t.getInfo().getTitle()
|
||||
+ "**\n"
|
||||
+ TrackFormatter.formatDuration(t.getInfo().getLength())))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
66
src/main/java/creator/loki/feature/music/util/QueueView.java
Normal file
66
src/main/java/creator/loki/feature/music/util/QueueView.java
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.music.manage.TrackScheduler;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
public final class QueueView {
|
||||
private static final Color BLURPLE = new Color(88, 101, 242);
|
||||
|
||||
private QueueView() {}
|
||||
|
||||
public static MessageEmbed queue(final TrackScheduler scheduler) {
|
||||
var cur = scheduler.getCurrentTrack();
|
||||
var q = scheduler.getQueue();
|
||||
if (cur == null && q.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(BLURPLE);
|
||||
if (cur != null) {
|
||||
var ci = cur.getInfo();
|
||||
eb.addField("Сейчас играет", "**" + ci.getTitle() + "**\n" + ci.getAuthor(), false);
|
||||
}
|
||||
if (!q.isEmpty()) {
|
||||
eb.addField("В очереди (" + q.size() + ")", queueList(q), false);
|
||||
}
|
||||
return eb.build();
|
||||
}
|
||||
|
||||
public static MessageEmbed nowPlaying(final TrackScheduler scheduler, final Link link) {
|
||||
var track = scheduler.getCurrentTrack();
|
||||
if (track == null) {
|
||||
return null;
|
||||
}
|
||||
var pos =
|
||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0L;
|
||||
return EmbedFactory.create()
|
||||
.withTitle("Сейчас играет")
|
||||
.withColor(BLURPLE)
|
||||
.addField("Название", "**" + track.getInfo().getTitle() + "**", false)
|
||||
.addField("Автор", track.getInfo().getAuthor(), true)
|
||||
.addField(
|
||||
"Длительность", TrackFormatter.progressBar(pos, track.getInfo().getLength()), true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String queueList(final List<Track> q) {
|
||||
var sb = new StringBuilder();
|
||||
var show = Math.min(q.size(), 10);
|
||||
for (int i = 0; i < show; i++) {
|
||||
sb.append("`")
|
||||
.append(i + 1)
|
||||
.append(".` **")
|
||||
.append(q.get(i).getInfo().getTitle())
|
||||
.append("**\n");
|
||||
}
|
||||
if (q.size() > show) {
|
||||
sb.append("... и ещё **").append(q.size() - show).append("** треков");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
public final class TrackFormatter {
|
||||
private TrackFormatter() {}
|
||||
|
||||
public static String formatDuration(final long millis) {
|
||||
long totalSec = millis / 1000;
|
||||
long hours = totalSec / 3600;
|
||||
long minutes = (totalSec % 3600) / 60;
|
||||
long secs = totalSec % 60;
|
||||
if (hours > 0) {
|
||||
return String.format("%d:%02d:%02d", hours, minutes, secs);
|
||||
}
|
||||
return String.format("%d:%02d", minutes, secs);
|
||||
}
|
||||
|
||||
public static String progressBar(final long pos, final long duration) {
|
||||
long dur = duration <= 0 ? 1 : duration;
|
||||
int total = 20;
|
||||
int filled = (int) ((double) pos / dur * total);
|
||||
var bar = new StringBuilder("\u2503");
|
||||
for (int i = 0; i < total; i++) {
|
||||
bar.append(i < filled ? "\u2588" : "\u2591");
|
||||
}
|
||||
var p = String.format("%d:%02d", pos / 60000, (pos / 1000) % 60);
|
||||
var d = String.format("%d:%02d", dur / 60000, (dur / 1000) % 60);
|
||||
return bar + "\u2503 " + p + " / " + d;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
public final class TrackQuery {
|
||||
private TrackQuery() {}
|
||||
|
||||
public static boolean isUrl(final String raw) {
|
||||
return raw.startsWith("http://") || raw.startsWith("https://");
|
||||
}
|
||||
|
||||
public static String build(final String source, final String raw) {
|
||||
if (isUrl(raw)) {
|
||||
return raw;
|
||||
}
|
||||
if (raw.startsWith("ytsearch:")
|
||||
|| raw.startsWith("ytmsearch:")
|
||||
|| raw.startsWith("scsearch:")) {
|
||||
return raw;
|
||||
}
|
||||
if ("sc".equals(source)) {
|
||||
return "scsearch:" + raw;
|
||||
}
|
||||
return "ytsearch:" + raw;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
|
||||
public final class VoiceCheck {
|
||||
private VoiceCheck() {}
|
||||
|
||||
public static Member requireChannel(final SlashCommandInteractionEvent ev) {
|
||||
var member = ev.getMember();
|
||||
if (member != null && member.getVoiceState().inAudioChannel()) {
|
||||
return member;
|
||||
}
|
||||
ev.replyEmbeds(EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Member requireChannel(final MessageChannel ch, final Member member) {
|
||||
if (member != null && member.getVoiceState().inAudioChannel()) {
|
||||
return member;
|
||||
}
|
||||
ch.sendMessageEmbeds(MusicEmbeds.err("Зайди в голосовой канал сначала.").build()).queue();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package creator.loki.feature.music.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
|
||||
public final class VolumeControl {
|
||||
private VolumeControl() {}
|
||||
|
||||
public static int current(final Link link) {
|
||||
return link != null && link.getCachedPlayer() != null
|
||||
? link.getCachedPlayer().getVolume()
|
||||
: 100;
|
||||
}
|
||||
|
||||
public static int clamp(final int vol) {
|
||||
return Math.max(0, Math.min(100, vol));
|
||||
}
|
||||
|
||||
public static void set(final Link link, final int vol, final Consumer<Throwable> onError) {
|
||||
if (link != null && link.getCachedPlayer() != null) {
|
||||
link.createOrUpdatePlayer().setVolume(vol).subscribe(null, onError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ import static creator.loki.feature.profile.render.RankCardConstants.GREY;
|
|||
import static creator.loki.feature.profile.render.RankCardConstants.H;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.OVERLAY;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.PAD;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.STATS_Y;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.STAT_GAP;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.STAT_H;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.STATS_Y;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.W;
|
||||
import static creator.loki.feature.profile.render.RankCardConstants.WHITE;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,13 @@ import creator.loki.command.CommandCategories;
|
|||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
public class RadioCommand implements Command {
|
||||
private final RadioManager radioManager;
|
||||
private final RadioSubcommands subcommands;
|
||||
|
||||
@Inject
|
||||
public RadioCommand(RadioManager radioManager) {
|
||||
this.radioManager = radioManager;
|
||||
this.subcommands = new RadioSubcommands(radioManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -41,146 +39,29 @@ public class RadioCommand implements Command {
|
|||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
Guild guild = context.getGuild();
|
||||
if (guild == null) {
|
||||
if (context.getGuild() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> args = context.getArgs();
|
||||
if (args.isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Использование: `" + getUsage() + "`"))
|
||||
.queue();
|
||||
sendUsage(context);
|
||||
return;
|
||||
}
|
||||
|
||||
String sub = args.get(0).toLowerCase();
|
||||
switch (sub) {
|
||||
case "set":
|
||||
handleSet(context, args);
|
||||
break;
|
||||
case "on":
|
||||
handleOn(context);
|
||||
break;
|
||||
case "off":
|
||||
handleOff(context);
|
||||
break;
|
||||
case "volume":
|
||||
handleVolume(context, args);
|
||||
break;
|
||||
default:
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Радио", "Неизвестная подкоманда. Использование: `" + getUsage() + "`"))
|
||||
.queue();
|
||||
break;
|
||||
switch (args.get(0).toLowerCase()) {
|
||||
case "set" -> subcommands.set(context, args);
|
||||
case "on" -> subcommands.on(context);
|
||||
case "off" -> subcommands.off(context);
|
||||
case "volume" -> subcommands.volume(context, args);
|
||||
default -> sendUsage(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSet(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Укажи URL: `!radio set <url>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
String url = args.get(1);
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Радио", "URL должен начинаться с http:// или https://"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
radioManager.setRadio(context.getGuild().getIdLong(), url);
|
||||
private void sendUsage(final CommandContext context) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.success("Радио", "URL радио потока установлен."))
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Использование: `" + getUsage() + "`"))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleOn(final CommandContext context) {
|
||||
if (!radioManager.hasRadioBot()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Radio бот не настроен."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Member member = context.getAuthor();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
long guildId = context.getGuild().getIdLong();
|
||||
String url = radioManager.getRadioUrl(guildId);
|
||||
if (url == null) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Радио", "Сначала установи URL: `!radio set <url>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
long channelId = member.getVoiceState().getChannel().getIdLong();
|
||||
Thread.startVirtualThread(() -> radioManager.startRadio(guildId, channelId));
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success("Радио", "Запущено радио в твоём голосовом канале."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleOff(final CommandContext context) {
|
||||
if (!radioManager.hasRadioBot()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Radio бот не настроен."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
radioManager.stopRadio(context.getGuild().getIdLong());
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.success("Радио", "Радио остановлено."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleVolume(final CommandContext context, final List<String> args) {
|
||||
long guildId = context.getGuild().getIdLong();
|
||||
if (args.size() < 2) {
|
||||
int vol = radioManager.getVolume(guildId);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Радио", "Текущая громкость: **" + vol + "%**"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int vol = Integer.parseInt(args.get(1));
|
||||
radioManager.setVolume(guildId, vol);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success("Радио", "Громкость установлена на **" + vol + "%**"))
|
||||
.queue();
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Укажи число от 0 до 100."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
package creator.loki.feature.radio.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
/** Обработчики подкоманд префиксной команды радио. */
|
||||
public class RadioSubcommands {
|
||||
private final RadioManager radioManager;
|
||||
|
||||
public RadioSubcommands(RadioManager radioManager) {
|
||||
this.radioManager = radioManager;
|
||||
}
|
||||
|
||||
public void set(final CommandContext context, final List<String> args) {
|
||||
if (args.size() < 2) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Укажи URL: `!radio set <url>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
String url = args.get(1);
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Радио", "URL должен начинаться с http:// или https://"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
radioManager.setRadio(context.getGuild().getIdLong(), url);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.success("Радио", "URL радио потока установлен."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void on(final CommandContext context) {
|
||||
if (!radioManager.hasRadioBot()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Radio бот не настроен."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Member member = context.getAuthor();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
long guildId = context.getGuild().getIdLong();
|
||||
String url = radioManager.getRadioUrl(guildId);
|
||||
if (url == null) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Радио", "Сначала установи URL: `!radio set <url>`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
long channelId = member.getVoiceState().getChannel().getIdLong();
|
||||
Thread.startVirtualThread(() -> radioManager.startRadio(guildId, channelId));
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success("Радио", "Запущено радио в твоём голосовом канале."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void off(final CommandContext context) {
|
||||
if (!radioManager.hasRadioBot()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Radio бот не настроен."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
radioManager.stopRadio(context.getGuild().getIdLong());
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.success("Радио", "Радио остановлено."))
|
||||
.queue();
|
||||
}
|
||||
|
||||
public void volume(final CommandContext context, final List<String> args) {
|
||||
long guildId = context.getGuild().getIdLong();
|
||||
if (args.size() < 2) {
|
||||
int vol = radioManager.getVolume(guildId);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("Радио", "Текущая громкость: **" + vol + "%**"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int vol = Integer.parseInt(args.get(1));
|
||||
radioManager.setVolume(guildId, vol);
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success("Радио", "Громкость установлена на **" + vol + "%**"))
|
||||
.queue();
|
||||
} catch (NumberFormatException e) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Укажи число от 0 до 100."))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package creator.loki.feature.radio.listener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
|
||||
/** Обработчики префиксных команд радио-бота. */
|
||||
public class RadioBotCommands {
|
||||
private final RadioManager radioManager;
|
||||
|
||||
public RadioBotCommands(RadioManager radioManager) {
|
||||
this.radioManager = radioManager;
|
||||
}
|
||||
|
||||
public void play(final MessageReceivedEvent event, final List<String> args) {
|
||||
Member member = requireVoiceChannel(event);
|
||||
if (member == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.isEmpty()) {
|
||||
event.getChannel().sendMessage("❌ Укажи URL радиостанции.").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = args.get(0);
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
event.getChannel().sendMessage("❌ URL должен начинаться с http:// или https://").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = event.getGuild();
|
||||
radioManager.setRadio(guild.getIdLong(), url);
|
||||
|
||||
VoiceChannel vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||
Thread.startVirtualThread(() -> radioManager.startRadio(guild.getIdLong(), vc.getIdLong()));
|
||||
|
||||
event.getChannel().sendMessage("📻 Радио запущено: **" + url + "**").queue();
|
||||
}
|
||||
|
||||
public void stop(final MessageReceivedEvent event) {
|
||||
Member member = requireVoiceChannel(event);
|
||||
if (member == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
radioManager.stopRadio(event.getGuild().getIdLong());
|
||||
event.getChannel().sendMessage("⏹️ Радио остановлено.").queue();
|
||||
}
|
||||
|
||||
public void volume(final MessageReceivedEvent event, final List<String> args) {
|
||||
long guildId = event.getGuild().getIdLong();
|
||||
if (args.isEmpty()) {
|
||||
int vol = radioManager.getVolume(guildId);
|
||||
event.getChannel().sendMessage("🔊 Текущая громкость: **" + vol + "%**").queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
radioManager.setVolume(guildId, vol);
|
||||
event.getChannel().sendMessage("🔊 Громкость установлена на **" + vol + "%**").queue();
|
||||
} catch (NumberFormatException e) {
|
||||
event.getChannel().sendMessage("❌ Укажи число от 0 до 100.").queue();
|
||||
}
|
||||
}
|
||||
|
||||
public void nowPlaying(final MessageReceivedEvent event) {
|
||||
long guildId = event.getGuild().getIdLong();
|
||||
String url = radioManager.getRadioUrl(guildId);
|
||||
if (url == null || !radioManager.isRadioActive(guildId)) {
|
||||
event.getChannel().sendMessage("📭 Радио сейчас не играет.").queue();
|
||||
return;
|
||||
}
|
||||
event.getChannel().sendMessage("📻 **Сейчас играет радио:** " + url).queue();
|
||||
}
|
||||
|
||||
public void help(final MessageReceivedEvent event) {
|
||||
event
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00AAFF)
|
||||
.setTitle("📻 Radio Bot Commands")
|
||||
.setDescription(
|
||||
"""
|
||||
`r!play <URL>` — Запустить радио
|
||||
`r!stop` — Остановить радио
|
||||
`r!volume [0-100]` — Громкость
|
||||
`r!np` — Что играет
|
||||
`r!help` — Показать это сообщение
|
||||
""")
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private Member requireVoiceChannel(final MessageReceivedEvent event) {
|
||||
Member member = event.getMember();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
event.getChannel().sendMessage("❌ Зайди в голосовой канал сначала.").queue();
|
||||
return null;
|
||||
}
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,24 +3,18 @@ package creator.loki.feature.radio.listener;
|
|||
import java.util.List;
|
||||
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class RadioBotListener extends ListenerAdapter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RadioBotListener.class);
|
||||
private static final String PREFIX = "r!";
|
||||
|
||||
private final RadioManager radioManager;
|
||||
private final RadioBotCommands commands;
|
||||
|
||||
public RadioBotListener(RadioManager radioManager) {
|
||||
this.radioManager = radioManager;
|
||||
this.commands = new RadioBotCommands(radioManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -50,115 +44,14 @@ public class RadioBotListener extends ListenerAdapter {
|
|||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "play":
|
||||
handlePlay(event, args);
|
||||
break;
|
||||
case "stop":
|
||||
handleStop(event);
|
||||
break;
|
||||
case "volume":
|
||||
handleVolume(event, args);
|
||||
break;
|
||||
case "np":
|
||||
case "nowplaying":
|
||||
handleNowPlaying(event);
|
||||
break;
|
||||
case "help":
|
||||
case "commands":
|
||||
handleHelp(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case "play" -> commands.play(event, args);
|
||||
case "stop" -> commands.stop(event);
|
||||
case "volume" -> commands.volume(event, args);
|
||||
case "np", "nowplaying" -> commands.nowPlaying(event);
|
||||
case "help", "commands" -> commands.help(event);
|
||||
default -> {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Member requireVoiceChannel(final MessageReceivedEvent event) {
|
||||
Member member = event.getMember();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
event.getChannel().sendMessage("❌ Зайди в голосовой канал сначала.").queue();
|
||||
return null;
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
private void handlePlay(final MessageReceivedEvent event, final List<String> args) {
|
||||
Member member = requireVoiceChannel(event);
|
||||
if (member == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.isEmpty()) {
|
||||
event.getChannel().sendMessage("❌ Укажи URL радиостанции.").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = args.get(0);
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
event.getChannel().sendMessage("❌ URL должен начинаться с http:// или https://").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = event.getGuild();
|
||||
radioManager.setRadio(guild.getIdLong(), url);
|
||||
|
||||
VoiceChannel vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||
Thread.startVirtualThread(() -> radioManager.startRadio(guild.getIdLong(), vc.getIdLong()));
|
||||
|
||||
event.getChannel().sendMessage("📻 Радио запущено: **" + url + "**").queue();
|
||||
}
|
||||
|
||||
private void handleStop(final MessageReceivedEvent event) {
|
||||
Member member = requireVoiceChannel(event);
|
||||
if (member == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
radioManager.stopRadio(event.getGuild().getIdLong());
|
||||
event.getChannel().sendMessage("⏹️ Радио остановлено.").queue();
|
||||
}
|
||||
|
||||
private void handleVolume(final MessageReceivedEvent event, final List<String> args) {
|
||||
long guildId = event.getGuild().getIdLong();
|
||||
if (args.isEmpty()) {
|
||||
int vol = radioManager.getVolume(guildId);
|
||||
event.getChannel().sendMessage("🔊 Текущая громкость: **" + vol + "%**").queue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
radioManager.setVolume(guildId, vol);
|
||||
event.getChannel().sendMessage("🔊 Громкость установлена на **" + vol + "%**").queue();
|
||||
} catch (NumberFormatException e) {
|
||||
event.getChannel().sendMessage("❌ Укажи число от 0 до 100.").queue();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNowPlaying(final MessageReceivedEvent event) {
|
||||
long guildId = event.getGuild().getIdLong();
|
||||
String url = radioManager.getRadioUrl(guildId);
|
||||
if (url == null || !radioManager.isRadioActive(guildId)) {
|
||||
event.getChannel().sendMessage("📭 Радио сейчас не играет.").queue();
|
||||
return;
|
||||
}
|
||||
event.getChannel().sendMessage("📻 **Сейчас играет радио:** " + url).queue();
|
||||
}
|
||||
|
||||
private void handleHelp(final MessageReceivedEvent event) {
|
||||
event
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00AAFF)
|
||||
.setTitle("📻 Radio Bot Commands")
|
||||
.setDescription(
|
||||
"""
|
||||
`r!play <URL>` — Запустить радио
|
||||
`r!stop` — Остановить радио
|
||||
`r!volume [0-100]` — Громкость
|
||||
`r!np` — Что играет
|
||||
`r!help` — Показать это сообщение
|
||||
""")
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -19,10 +16,11 @@ import org.slf4j.LoggerFactory;
|
|||
public class RadioManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RadioManager.class);
|
||||
|
||||
private final Map<Long, GuildRadioState> radioStates = new ConcurrentHashMap<>();
|
||||
private final Map<Long, RadioSession> radioSessions = new ConcurrentHashMap<>();
|
||||
private final QueueRepository queueRepository;
|
||||
private LavalinkManager lavalinkManager;
|
||||
private JDA radioJda;
|
||||
private RadioPlayer player;
|
||||
|
||||
@Inject
|
||||
public RadioManager(QueueRepository queueRepository) {
|
||||
|
|
@ -31,18 +29,26 @@ public class RadioManager {
|
|||
|
||||
public void setLavalinkManager(final LavalinkManager lm) {
|
||||
this.lavalinkManager = lm;
|
||||
rebuildPlayer();
|
||||
}
|
||||
|
||||
public void setRadioJda(JDA radioJda) {
|
||||
this.radioJda = radioJda;
|
||||
rebuildPlayer();
|
||||
}
|
||||
|
||||
private void rebuildPlayer() {
|
||||
if (radioJda != null && lavalinkManager != null) {
|
||||
this.player = new RadioPlayer(radioJda, lavalinkManager, logger);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRadio(final long guildId, String streamUrl) {
|
||||
radioStates.compute(
|
||||
radioSessions.compute(
|
||||
guildId,
|
||||
(id, state) -> {
|
||||
if (state == null) {
|
||||
state = new GuildRadioState();
|
||||
state = new RadioSession();
|
||||
}
|
||||
state.streamUrl = streamUrl;
|
||||
return state;
|
||||
|
|
@ -51,16 +57,16 @@ public class RadioManager {
|
|||
}
|
||||
|
||||
public String getRadioUrl(final long guildId) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
RadioSession state = radioSessions.get(guildId);
|
||||
return state != null ? state.streamUrl : null;
|
||||
}
|
||||
|
||||
public void startRadio(final long guildId, final long channelId) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
RadioSession state = radioSessions.get(guildId);
|
||||
if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (radioJda == null || lavalinkManager == null) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -70,44 +76,14 @@ public class RadioManager {
|
|||
stopRadio(guildId);
|
||||
}
|
||||
|
||||
GuildRadioState finalState = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState());
|
||||
RadioSession finalState = radioSessions.computeIfAbsent(guildId, id -> new RadioSession());
|
||||
finalState.streamUrl = url;
|
||||
finalState.volume = vol;
|
||||
|
||||
Guild guild = radioJda.getGuildById(guildId);
|
||||
if (guild == null) {
|
||||
logger.warn("Radio: guild {} not found in radio bot cache", guildId);
|
||||
return;
|
||||
}
|
||||
var vc = guild.getVoiceChannelById(channelId);
|
||||
if (vc == null) {
|
||||
logger.warn("Radio: voice channel {} not found in guild {}", channelId, guildId);
|
||||
return;
|
||||
}
|
||||
var self = guild.getMember(guild.getJDA().getSelfUser());
|
||||
if (self == null) {
|
||||
logger.warn("Radio: self member is null for guild {}", guildId);
|
||||
return;
|
||||
}
|
||||
if (!self.hasPermission(vc, net.dv8tion.jda.api.Permission.VOICE_CONNECT)) {
|
||||
logger.warn("Radio: no CONNECT permission in channel {}/{}", channelId, guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
var link = lavalinkManager.getOrCreateLink(guildId);
|
||||
guild.getJDA().getDirectAudioController().connect(vc);
|
||||
|
||||
var mm = new GuildMusicManager(guildId, lavalinkManager, null);
|
||||
mm.init();
|
||||
finalState.musicManager = mm;
|
||||
lavalinkManager
|
||||
.getOrCreateLink(guildId)
|
||||
.loadItem(url)
|
||||
.subscribe(new RadioStreamLoader(link, mm, guildId, vol, url, logger));
|
||||
finalState.musicManager = player.connect(guildId, channelId, vol, url);
|
||||
}
|
||||
|
||||
public void stopRadio(final long guildId) {
|
||||
GuildRadioState state = radioStates.remove(guildId);
|
||||
RadioSession state = radioSessions.remove(guildId);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -115,23 +91,14 @@ public class RadioManager {
|
|||
state.musicManager.stop();
|
||||
state.musicManager.destroy();
|
||||
}
|
||||
if (lavalinkManager != null) {
|
||||
Link link = lavalinkManager.getClient().getLinkIfCached(guildId);
|
||||
if (link != null) {
|
||||
link.destroy().subscribe();
|
||||
}
|
||||
}
|
||||
if (radioJda != null) {
|
||||
Guild guild = radioJda.getGuildById(guildId);
|
||||
if (guild != null) {
|
||||
guild.getAudioManager().closeAudioConnection();
|
||||
}
|
||||
if (player != null) {
|
||||
player.disconnect(guildId);
|
||||
}
|
||||
LoggerDecorator.music(logger, "Radio stopped for guild " + guildId);
|
||||
}
|
||||
|
||||
public boolean isRadioActive(final long guildId) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
RadioSession state = radioSessions.get(guildId);
|
||||
return state != null && state.streamUrl != null;
|
||||
}
|
||||
|
||||
|
|
@ -144,12 +111,12 @@ public class RadioManager {
|
|||
}
|
||||
|
||||
public int getVolume(final long guildId) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
RadioSession state = radioSessions.get(guildId);
|
||||
return state != null ? state.volume : 100;
|
||||
}
|
||||
|
||||
public void setVolume(final long guildId, int volume) {
|
||||
GuildRadioState state = radioStates.get(guildId);
|
||||
RadioSession state = radioSessions.get(guildId);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -163,10 +130,4 @@ public class RadioManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class GuildRadioState {
|
||||
String streamUrl;
|
||||
GuildMusicManager musicManager;
|
||||
int volume = 100;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package creator.loki.feature.radio.service;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/** Подключение и отключение радио-бота в голосовых каналах. */
|
||||
public class RadioPlayer {
|
||||
private final JDA radioJda;
|
||||
private final LavalinkManager lavalinkManager;
|
||||
private final Logger logger;
|
||||
|
||||
public RadioPlayer(JDA radioJda, LavalinkManager lavalinkManager, Logger logger) {
|
||||
this.radioJda = radioJda;
|
||||
this.lavalinkManager = lavalinkManager;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public GuildMusicManager connect(
|
||||
final long guildId, final long channelId, final int volume, final String url) {
|
||||
Guild guild = radioJda.getGuildById(guildId);
|
||||
if (guild == null) {
|
||||
logger.warn("Radio: guild {} not found in radio bot cache", guildId);
|
||||
return null;
|
||||
}
|
||||
var vc = guild.getVoiceChannelById(channelId);
|
||||
if (vc == null) {
|
||||
logger.warn("Radio: voice channel {} not found in guild {}", channelId, guildId);
|
||||
return null;
|
||||
}
|
||||
var self = guild.getMember(guild.getJDA().getSelfUser());
|
||||
if (self == null) {
|
||||
logger.warn("Radio: self member is null for guild {}", guildId);
|
||||
return null;
|
||||
}
|
||||
if (!self.hasPermission(vc, Permission.VOICE_CONNECT)) {
|
||||
logger.warn("Radio: no CONNECT permission in channel {}/{}", channelId, guildId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var link = lavalinkManager.getOrCreateLink(guildId);
|
||||
guild.getJDA().getDirectAudioController().connect(vc);
|
||||
|
||||
var mm = new GuildMusicManager(guildId, lavalinkManager, null);
|
||||
mm.init();
|
||||
lavalinkManager
|
||||
.getOrCreateLink(guildId)
|
||||
.loadItem(url)
|
||||
.subscribe(new RadioStreamLoader(link, mm, guildId, volume, url, logger));
|
||||
return mm;
|
||||
}
|
||||
|
||||
public void disconnect(final long guildId) {
|
||||
Link link = lavalinkManager.getClient().getLinkIfCached(guildId);
|
||||
if (link != null) {
|
||||
link.destroy().subscribe();
|
||||
}
|
||||
Guild guild = radioJda.getGuildById(guildId);
|
||||
if (guild != null) {
|
||||
guild.getAudioManager().closeAudioConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package creator.loki.feature.radio.service;
|
||||
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
|
||||
/** Состояние радио для одной гильдии. */
|
||||
class RadioSession {
|
||||
String streamUrl;
|
||||
GuildMusicManager musicManager;
|
||||
int volume = 100;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue