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) {}
|
String prefix, String activity, int healthHttpPort, String superAdminId, String dbPath) {}
|
||||||
|
|
||||||
public record AiContext(
|
public record AiContext(
|
||||||
String deepseekApiKey, String aiChannelId, int aiContextMsgs, int aiMsgLength,
|
String deepseekApiKey,
|
||||||
|
String aiChannelId,
|
||||||
|
int aiContextMsgs,
|
||||||
|
int aiMsgLength,
|
||||||
int aiContextChars) {}
|
int aiContextChars) {}
|
||||||
|
|
||||||
public record AiDossier(int aiDossierMsgs, int aiDossierUpdateHours, String aiDebateChannelId) {}
|
public record AiDossier(int aiDossierMsgs, int aiDossierUpdateHours, String aiDebateChannelId) {}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@ public final class ConfigLoader {
|
||||||
return new BotConfig(
|
return new BotConfig(
|
||||||
new BotConfig.Tokens(mainToken, musicToken, radioToken),
|
new BotConfig.Tokens(mainToken, musicToken, radioToken),
|
||||||
new BotConfig.BotSettings(prefix, activity, healthHttpPort, superAdminId, dbPath),
|
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),
|
new BotConfig.AiDossier(aiDossierMsgs, aiDossierUpdateHours, aiDebateChannelId),
|
||||||
lavalinkConfig,
|
lavalinkConfig,
|
||||||
externalUrl);
|
externalUrl);
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,12 @@ import org.slf4j.LoggerFactory;
|
||||||
public class DatabaseMigration {
|
public class DatabaseMigration {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
||||||
private final Database database;
|
private final Database database;
|
||||||
|
private final JsonMigration jsonMigration;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
public DatabaseMigration(Database database) {
|
public DatabaseMigration(Database database) {
|
||||||
this.database = database;
|
this.database = database;
|
||||||
|
this.jsonMigration = new JsonMigration(database, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void migrateFromJson(final String jsonPath) {
|
public void migrateFromJson(final String jsonPath) {
|
||||||
|
|
@ -32,24 +34,33 @@ public class DatabaseMigration {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
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'");
|
try (var chk = database.prepare("SELECT value FROM config WHERE key = 'migrated'");
|
||||||
var rs = chk.executeQuery()) {
|
var rs = chk.executeQuery()) {
|
||||||
if (rs.next() && "true".equals(rs.getString("value"))) {
|
return rs.next() && "true".equals(rs.getString("value"));
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (SQLException ignored) {
|
} catch (SQLException ignored) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
migrateUserStats(data);
|
}
|
||||||
migrateReputation(data);
|
|
||||||
migrateShipPairs(data);
|
private void markMigrated() {
|
||||||
migrateCustomRoles(data);
|
|
||||||
var sql = "INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')";
|
var sql = "INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')";
|
||||||
try (var p = database.prepare(sql)) {
|
try (var p = database.prepare(sql)) {
|
||||||
p.executeUpdate();
|
p.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LoggerDecorator.error(logger, "Ошибка сохранения статуса миграции", e);
|
LoggerDecorator.error(logger, "Ошибка сохранения статуса миграции", e);
|
||||||
}
|
}
|
||||||
LoggerDecorator.success(logger, "Миграция завершена успешно");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
@ -62,108 +73,4 @@ public class DatabaseMigration {
|
||||||
return null;
|
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 {
|
public final class FeatureTables {
|
||||||
private 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) {
|
public static void create(final Database database) {
|
||||||
database.execute(ECONOMY);
|
ModerationTables.create(database);
|
||||||
database.execute(MUSIC_QUEUE);
|
InteractionTables.create(database);
|
||||||
database.execute(WARNINGS);
|
EconomyTables.create(database);
|
||||||
database.execute(MODERATION_CASES);
|
VoiceTables.create(database);
|
||||||
database.execute(ACTIVE_MUTES);
|
CounterTables.create(database);
|
||||||
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);
|
|
||||||
createIndexes(database);
|
createIndexes(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createIndexes(final Database database) {
|
private static void createIndexes(final Database database) {
|
||||||
for (var sql :
|
ModerationTables.createIndexes(database);
|
||||||
new String[] {
|
EconomyTables.createIndexes(database);
|
||||||
"CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)",
|
VoiceTables.createIndexes(database);
|
||||||
"CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)",
|
CounterTables.createIndexes(database);
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.awt.Color;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
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.entities.Guild;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
|
||||||
|
|
||||||
class SetupDashboardBuilder {
|
class SetupDashboardBuilder {
|
||||||
private static final Color BLURPLE = new Color(88, 101, 242);
|
private static final Color BLURPLE = new Color(88, 101, 242);
|
||||||
|
|
@ -90,91 +81,7 @@ class SetupDashboardBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLevelRolesSection(final EmbedBuilder embed) {
|
private void addLevelRolesSection(final EmbedBuilder embed) {
|
||||||
Map<String, String> allConfig = configRepo.getAll();
|
Map<Integer, String> levelRoles = LevelRoleConfig.byLevel(configRepo.getAll());
|
||||||
long levelRoleCount =
|
embed.addField("🏆 Роли за уровни", "Настроено ролей: **" + levelRoles.size() + "**", false);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.awt.Color;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
|
@ -115,14 +114,7 @@ class SetupPanelBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLevelRolesPanel(Guild guild, final InteractionHook hook) {
|
void showLevelRolesPanel(Guild guild, final InteractionHook hook) {
|
||||||
Map<String, String> allConfig = configRepo.getAll();
|
Map<Integer, String> levelRoles = LevelRoleConfig.byLevel(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));
|
|
||||||
|
|
||||||
EmbedBuilder embed = new EmbedBuilder().setTitle("🏆 Роли за уровни").setColor(BLURPLE);
|
EmbedBuilder embed = new EmbedBuilder().setTitle("🏆 Роли за уровни").setColor(BLURPLE);
|
||||||
|
|
||||||
|
|
@ -130,15 +122,9 @@ class SetupPanelBuilder {
|
||||||
embed.setDescription("❌ Роли не настроены");
|
embed.setDescription("❌ Роли не настроены");
|
||||||
} else {
|
} else {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
levelRoles.entrySet().stream()
|
levelRoles.forEach(
|
||||||
.sorted(Map.Entry.comparingByKey())
|
(level, role) ->
|
||||||
.forEach(
|
sb.append("**Уровень ").append(level).append(":** `").append(role).append("`\n"));
|
||||||
e ->
|
|
||||||
sb.append("**Уровень ")
|
|
||||||
.append(e.getKey())
|
|
||||||
.append(":** `")
|
|
||||||
.append(e.getValue())
|
|
||||||
.append("`\n"));
|
|
||||||
embed.setDescription(sb.toString());
|
embed.setDescription(sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import com.google.inject.Singleton;
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
|
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.command.SlashCommandInteractionEvent;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
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.EntitySelectInteractionEvent;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
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.hooks.ListenerAdapter;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
||||||
|
|
@ -20,6 +20,8 @@ public class SetupSlashHandler extends ListenerAdapter {
|
||||||
private final ConfigRepository configRepo;
|
private final ConfigRepository configRepo;
|
||||||
private final SetupPanelBuilder panelBuilder;
|
private final SetupPanelBuilder panelBuilder;
|
||||||
private final SetupDashboardBuilder dashBuilder;
|
private final SetupDashboardBuilder dashBuilder;
|
||||||
|
private final SetupModals modals;
|
||||||
|
private final SetupInteractions interactions;
|
||||||
private volatile boolean registered = false;
|
private volatile boolean registered = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -27,6 +29,8 @@ public class SetupSlashHandler extends ListenerAdapter {
|
||||||
this.configRepo = configRepo;
|
this.configRepo = configRepo;
|
||||||
this.panelBuilder = new SetupPanelBuilder(configRepo);
|
this.panelBuilder = new SetupPanelBuilder(configRepo);
|
||||||
this.dashBuilder = new SetupDashboardBuilder(configRepo);
|
this.dashBuilder = new SetupDashboardBuilder(configRepo);
|
||||||
|
this.modals = new SetupModals(configRepo);
|
||||||
|
this.interactions = new SetupInteractions(configRepo, panelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -63,89 +67,41 @@ public class SetupSlashHandler extends ListenerAdapter {
|
||||||
case "setup_voice" -> panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
case "setup_voice" -> panelBuilder.showVoicePanel(event.getGuild(), event.getHook());
|
||||||
case "setup_counter" -> panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
case "setup_counter" -> panelBuilder.showCounterPanel(event.getGuild(), event.getHook());
|
||||||
case "setup_logs" -> panelBuilder.showLoggingPanel(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_back" -> showDashboard(event.getGuild(), event.getHook());
|
||||||
case "setup_counter_format" -> dashBuilder.openCounterFormatModal(event);
|
case "setup_counter_format" -> modals.openCounterFormatModal(event);
|
||||||
case "setup_levelrole_add" -> dashBuilder.openLevelRoleAddModal(event);
|
case "setup_levelrole_add" -> modals.openLevelRoleAddModal(event);
|
||||||
case "setup_levelrole_remove" -> dashBuilder.openLevelRoleRemoveSelect(event);
|
case "setup_levelrole_remove" -> modals.openLevelRoleRemoveSelect(event);
|
||||||
default -> {}
|
default -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEntitySelectInteraction(@NotNull final EntitySelectInteractionEvent event) {
|
public void onEntitySelectInteraction(@NotNull final EntitySelectInteractionEvent event) {
|
||||||
String id = event.getComponentId();
|
if (!event.getComponentId().startsWith("setup_")) {
|
||||||
if (!id.startsWith("setup_")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.deferEdit().queue();
|
event.deferEdit().queue();
|
||||||
|
interactions.onEntitySelect(event);
|
||||||
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 -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStringSelectInteraction(@NotNull final StringSelectInteractionEvent event) {
|
public void onStringSelectInteraction(@NotNull final StringSelectInteractionEvent event) {
|
||||||
String id = event.getComponentId();
|
if (!event.getComponentId().startsWith("setup_")) {
|
||||||
if (!id.startsWith("setup_")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.deferEdit().queue();
|
event.deferEdit().queue();
|
||||||
|
interactions.onStringSelect(event);
|
||||||
if ("setup_levelrole_remove_select".equals(id)) {
|
|
||||||
String selected = event.getValues().get(0);
|
|
||||||
configRepo.delete("level_role_" + selected);
|
|
||||||
panelBuilder.showLevelRolesPanel(event.getGuild(), event.getHook());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onModalInteraction(@NotNull final ModalInteractionEvent event) {
|
public void onModalInteraction(@NotNull final ModalInteractionEvent event) {
|
||||||
String id = event.getModalId();
|
if (!event.getModalId().startsWith("setup_")) {
|
||||||
if (!id.startsWith("setup_")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.deferEdit().queue();
|
event.deferEdit().queue();
|
||||||
|
interactions.onModal(event);
|
||||||
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 -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDashboard(Guild guild, final InteractionHook hook) {
|
private void showDashboard(Guild guild, final InteractionHook hook) {
|
||||||
|
|
@ -153,19 +109,4 @@ public class SetupSlashHandler extends ListenerAdapter {
|
||||||
.setComponents(panelBuilder.buildDashboardButtons())
|
.setComponents(panelBuilder.buildDashboardButtons())
|
||||||
.queue();
|
.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) {
|
if (balance < amount) {
|
||||||
sendError(
|
sendError(
|
||||||
ctx,
|
ctx,
|
||||||
"Недостаточно " + (isDep ? "наличных" : "в банке")
|
"Недостаточно " + (isDep ? "наличных" : "в банке") + "! У вас **" + balance + "** LC");
|
||||||
+ "! У вас **" + balance + "** LC");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isDep) {
|
if (isDep) {
|
||||||
|
|
@ -102,8 +101,11 @@ public class BankCommand implements Command {
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
EmbedTemplates.success(
|
EmbedTemplates.success(
|
||||||
isDep ? "Депозит" : "Снятие",
|
isDep ? "Депозит" : "Снятие",
|
||||||
"Ты " + (isDep ? "положил" : "снял")
|
"Ты "
|
||||||
+ " **" + amount + "** LC "
|
+ (isDep ? "положил" : "снял")
|
||||||
|
+ " **"
|
||||||
|
+ amount
|
||||||
|
+ "** LC "
|
||||||
+ (isDep ? "в банк!" : "из банка!")))
|
+ (isDep ? "в банк!" : "из банка!")))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
@ -123,8 +125,6 @@ public class BankCommand implements Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendError(final CommandContext ctx, final String msg) {
|
private void sendError(final CommandContext ctx, final String msg) {
|
||||||
ctx.getChannel()
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg))
|
|
||||||
.queue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
package creator.loki.feature.economy.command.job;
|
package creator.loki.feature.economy.command.job;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.economy.service.game.JobService;
|
import creator.loki.feature.economy.service.game.JobService;
|
||||||
|
|
||||||
|
|
@ -78,7 +75,7 @@ public class JobCommand implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JobService.JobInfo info = findJob(current);
|
JobService.JobInfo info = JobEmbeds.findJob(jobService.listJobs(), current);
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
|
|
@ -88,59 +85,15 @@ public class JobCommand implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context
|
context.getChannel().sendMessageEmbeds(JobEmbeds.current(info)).queue();
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleList(final CommandContext context) {
|
private void handleList(final CommandContext context) {
|
||||||
var jobs = jobService.listJobs();
|
context.getChannel().sendMessageEmbeds(JobEmbeds.list(jobService.listJobs())).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleJoin(final CommandContext context, final String userId, final String name) {
|
private void handleJoin(final CommandContext context, final String userId, final String name) {
|
||||||
var jobs = jobService.listJobs();
|
JobService.JobInfo target = JobEmbeds.findJob(jobService.listJobs(), name);
|
||||||
JobService.JobInfo target = null;
|
|
||||||
|
|
||||||
for (var job : jobs) {
|
|
||||||
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
|
|
||||||
target = job;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
|
|
@ -166,13 +119,4 @@ public class JobCommand implements Command {
|
||||||
+ "Используй `!work` чтобы начать работать."))
|
+ "Используй `!work` чтобы начать работать."))
|
||||||
.queue();
|
.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;
|
package creator.loki.feature.economy.command.minigame;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.economy.service.EconomyService;
|
import creator.loki.feature.economy.service.EconomyService;
|
||||||
import creator.loki.feature.economy.service.game.BlackjackService;
|
import creator.loki.feature.economy.service.game.BlackjackService;
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
|
||||||
|
|
||||||
public class BlackjackCommand implements Command {
|
public class BlackjackCommand implements Command {
|
||||||
|
|
||||||
|
|
@ -111,7 +108,7 @@ public class BlackjackCommand implements Command {
|
||||||
if (!state.isOver()) {
|
if (!state.isOver()) {
|
||||||
activeGames.add(userId);
|
activeGames.add(userId);
|
||||||
}
|
}
|
||||||
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
context.getChannel().sendMessageEmbeds(BlackjackEmbeds.game(state)).queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAction(
|
private void handleAction(
|
||||||
|
|
@ -129,50 +126,6 @@ public class BlackjackCommand implements Command {
|
||||||
if (state.isOver()) {
|
if (state.isOver()) {
|
||||||
activeGames.remove(userId);
|
activeGames.remove(userId);
|
||||||
}
|
}
|
||||||
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
|
context.getChannel().sendMessageEmbeds(BlackjackEmbeds.game(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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package creator.loki.feature.economy.repository;
|
||||||
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -11,57 +8,25 @@ import java.util.Optional;
|
||||||
|
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
import creator.loki.feature.economy.domain.EconomyAccount;
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class SqlEconomyRepository implements EconomyRepository {
|
public class SqlEconomyRepository implements EconomyRepository {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
|
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 =
|
private final Database database;
|
||||||
"user_id, cash, bank, total_earned, total_spent,"
|
|
||||||
+ " last_daily, daily_streak, fishing_luck,"
|
|
||||||
+ " efficiency, last_fish, job, last_work";
|
|
||||||
|
|
||||||
public SqlEconomyRepository(Database database) {
|
public SqlEconomyRepository(Database database) {
|
||||||
this.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
|
@Override
|
||||||
public Optional<EconomyAccount> findById(final String userId) {
|
public Optional<EconomyAccount> findById(final String userId) {
|
||||||
var sql = selectSql();
|
try (var stmt = database.prepare(EconomySql.selectSql())) {
|
||||||
try (var stmt = database.prepare(sql)) {
|
|
||||||
stmt.setString(1, userId);
|
stmt.setString(1, userId);
|
||||||
try (var rs = stmt.executeQuery()) {
|
try (var rs = stmt.executeQuery()) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
return Optional.of(mapResultSetToAccount(rs));
|
return Optional.of(EconomyRowMapper.map(rs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
|
@ -72,20 +37,19 @@ public class SqlEconomyRepository implements EconomyRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(final EconomyAccount account) {
|
public void save(final EconomyAccount account) {
|
||||||
var sql = upsertSql();
|
try (var stmt = database.prepare(EconomySql.upsertSql())) {
|
||||||
try (var stmt = database.prepare(sql)) {
|
|
||||||
stmt.setString(1, account.getUserId());
|
stmt.setString(1, account.getUserId());
|
||||||
stmt.setLong(2, account.getCash());
|
stmt.setLong(2, account.getCash());
|
||||||
stmt.setLong(3, account.getBank());
|
stmt.setLong(3, account.getBank());
|
||||||
stmt.setLong(4, account.getTotalEarned());
|
stmt.setLong(4, account.getTotalEarned());
|
||||||
stmt.setLong(5, account.getTotalSpent());
|
stmt.setLong(5, account.getTotalSpent());
|
||||||
setNullableString(stmt, 6, account.getLastDaily());
|
EconomySql.setNullableString(stmt, 6, account.getLastDaily());
|
||||||
stmt.setInt(7, account.getDailyStreak());
|
stmt.setInt(7, account.getDailyStreak());
|
||||||
stmt.setInt(8, account.getFishingLuck());
|
stmt.setInt(8, account.getFishingLuck());
|
||||||
stmt.setInt(9, account.getEfficiency());
|
stmt.setInt(9, account.getEfficiency());
|
||||||
setNullableString(stmt, 10, account.getLastFish());
|
EconomySql.setNullableString(stmt, 10, account.getLastFish());
|
||||||
setNullableString(stmt, 11, account.getJob());
|
EconomySql.setNullableString(stmt, 11, account.getJob());
|
||||||
setNullableString(stmt, 12, account.getLastWork());
|
EconomySql.setNullableString(stmt, 12, account.getLastWork());
|
||||||
stmt.executeUpdate();
|
stmt.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
logger.error("Error saving economy account for {}", account.getUserId(), e);
|
logger.error("Error saving economy account for {}", account.getUserId(), e);
|
||||||
|
|
@ -94,8 +58,7 @@ public class SqlEconomyRepository implements EconomyRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists(final String userId) {
|
public boolean exists(final String userId) {
|
||||||
var sql = "SELECT 1 FROM economy WHERE user_id = ?";
|
try (var stmt = database.prepare("SELECT 1 FROM economy WHERE user_id = ?")) {
|
||||||
try (var stmt = database.prepare(sql)) {
|
|
||||||
stmt.setString(1, userId);
|
stmt.setString(1, userId);
|
||||||
try (var rs = stmt.executeQuery()) {
|
try (var rs = stmt.executeQuery()) {
|
||||||
return rs.next();
|
return rs.next();
|
||||||
|
|
@ -108,17 +71,13 @@ public class SqlEconomyRepository implements EconomyRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<EconomyAccount> findTopByBalance(final int limit) {
|
public List<EconomyAccount> findTopByBalance(final int limit) {
|
||||||
var lim = clampLimit(limit);
|
var lim = EconomySql.clampLimit(limit);
|
||||||
var sql =
|
|
||||||
"SELECT "
|
|
||||||
+ COLUMNS
|
|
||||||
+ " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?";
|
|
||||||
var list = new ArrayList<EconomyAccount>();
|
var list = new ArrayList<EconomyAccount>();
|
||||||
try (var stmt = database.prepare(sql)) {
|
try (var stmt = database.prepare(EconomySql.topByBalanceSql())) {
|
||||||
stmt.setInt(1, lim);
|
stmt.setInt(1, lim);
|
||||||
try (var rs = stmt.executeQuery()) {
|
try (var rs = stmt.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
list.add(mapResultSetToAccount(rs));
|
list.add(EconomyRowMapper.map(rs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
|
@ -126,28 +85,4 @@ public class SqlEconomyRepository implements EconomyRepository {
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableList(list);
|
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;
|
package creator.loki.feature.economy.service;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
|
|
||||||
import creator.loki.feature.economy.domain.EconomyAccount;
|
import creator.loki.feature.economy.domain.EconomyAccount;
|
||||||
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
|
||||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class FishingService {
|
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 static final long COOLDOWN_MS = 7200000;
|
||||||
|
|
||||||
private final EconomyRepository repository;
|
private final EconomyRepository repository;
|
||||||
|
|
@ -39,65 +29,23 @@ public class FishingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int size;
|
FishingDifficulty diff = FishingDifficulty.from(difficulty);
|
||||||
int fishTypes;
|
if (diff == null) {
|
||||||
int baseReward;
|
return null;
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FishingBoard board = new FishingBoard(diff.getSize(), diff.getFishTypes());
|
||||||
ActiveFish session =
|
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);
|
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) {
|
public FishResult checkAnswer(final String userId, int answer) {
|
||||||
|
|
@ -118,34 +66,17 @@ public class FishingService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FishingDifficulty diff = FishingDifficulty.from(difficulty);
|
||||||
|
if (diff == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
EconomyAccount account = repository.getAccount(userId);
|
EconomyAccount account = repository.getAccount(userId);
|
||||||
long elapsed = System.currentTimeMillis() - session.startTime();
|
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;
|
long reward = 0;
|
||||||
if (answer == session.correctCount() && elapsed <= timeLimit) {
|
if (answer == session.correctCount() && elapsed <= diff.getTimeLimitMs()) {
|
||||||
double ratio = (double) elapsed / timeLimit;
|
reward = diff.computeReward(elapsed, account.getFishingLuck());
|
||||||
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
|
|
||||||
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
long newCash = account.getCash() + reward;
|
long newCash = account.getCash() + reward;
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,7 @@ public class GlazbobaCommand implements Command {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
loadingMsg.editMessage("🔐 Взламываю Discord...").queue();
|
loadingMsg.editMessage("🔐 Взламываю Discord...").queue();
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
loadingMsg
|
loadingMsg.editMessageEmbeds(buildProfileEmbed(target, context).build()).queue();
|
||||||
.editMessageEmbeds(buildProfileEmbed(target, context).build())
|
|
||||||
.queue();
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
|
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
|
||||||
}
|
}
|
||||||
|
|
@ -90,9 +88,7 @@ public class GlazbobaCommand implements Command {
|
||||||
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
|
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
|
||||||
.addField("🎭 Роли", roles, false)
|
.addField("🎭 Роли", roles, false)
|
||||||
.withColor(
|
.withColor(
|
||||||
target.getColors().getPrimary() != null
|
target.getColors().getPrimary() != null ? target.getColors().getPrimary() : Color.BLUE)
|
||||||
? target.getColors().getPrimary()
|
|
||||||
: Color.BLUE)
|
|
||||||
.withFooter("Запросил: " + context.getAuthor().getUser().getName());
|
.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;
|
package creator.loki.feature.help;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
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.Permission;
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
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.entities.channel.attribute.IAgeRestrictedChannel;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class HelpMenuHandler extends ListenerAdapter {
|
public class HelpMenuHandler extends ListenerAdapter {
|
||||||
private final CommandRegistry registry;
|
private final HelpMenuBuilder menuBuilder;
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public HelpMenuHandler(CommandRegistry registry, BotConfig config) {
|
public HelpMenuHandler(CommandRegistry registry, BotConfig config) {
|
||||||
this.registry = registry;
|
this.menuBuilder = new HelpMenuBuilder(registry, config);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,116 +37,44 @@ public class HelpMenuHandler extends ListenerAdapter {
|
||||||
|
|
||||||
String category = selectedValue.substring("category:".length());
|
String category = selectedValue.substring("category:".length());
|
||||||
|
|
||||||
// Проверка прав для категории "Настройка"
|
if (!canViewCategory(event, category)) {
|
||||||
if ("Настройка".equals(category)
|
|
||||||
&& event.getMember() != null
|
|
||||||
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
|
|
||||||
event.reply("❌ Эта категория доступна только администраторам!").setEphemeral(true).queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("Супер-Админ".equals(category)
|
var commandsByCategory = menuBuilder.groupByCategory();
|
||||||
&& !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));
|
|
||||||
|
|
||||||
List<Command> categoryCommands = commandsByCategory.get(category);
|
List<Command> categoryCommands = commandsByCategory.get(category);
|
||||||
if (categoryCommands == null || categoryCommands.isEmpty()) {
|
if (categoryCommands == null || categoryCommands.isEmpty()) {
|
||||||
event.reply("❌ В этой категории нет команд!").setEphemeral(true).queue();
|
event.reply("❌ В этой категории нет команд!").setEphemeral(true).queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаём embed для категории
|
|
||||||
EmbedFactory embed = createCategoryEmbed(category, categoryCommands, event);
|
|
||||||
|
|
||||||
// Пересоздаём селектор
|
|
||||||
StringSelectMenu.Builder menuBuilder =
|
|
||||||
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
|
|
||||||
|
|
||||||
boolean isAdmin =
|
boolean isAdmin =
|
||||||
event.getMember() != null && event.getMember().hasPermission(Permission.ADMINISTRATOR);
|
event.getMember() != null && event.getMember().hasPermission(Permission.ADMINISTRATOR);
|
||||||
boolean isSuper = config.getSuperAdminId().equals(event.getUser().getId());
|
boolean isSuper = config.getSuperAdminId().equals(event.getUser().getId());
|
||||||
boolean isNsfwChannel = isNsfw(event);
|
boolean isNsfwChannel = isNsfw(event);
|
||||||
|
|
||||||
commandsByCategory.keySet().stream()
|
event
|
||||||
.sorted()
|
.editMessageEmbeds(
|
||||||
.forEach(
|
HelpCategoryEmbed.build(category, categoryCommands, event, config).build())
|
||||||
cat -> {
|
.setComponents(
|
||||||
if (!HelpCategories.isCategoryVisible(cat, isAdmin, isSuper, isNsfwChannel)) {
|
ActionRow.of(
|
||||||
return;
|
menuBuilder.buildSelector(commandsByCategory, isAdmin, isSuper, isNsfwChannel)))
|
||||||
}
|
.queue();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EmbedFactory createCategoryEmbed(
|
private boolean canViewCategory(final StringSelectInteractionEvent event, final String category) {
|
||||||
final String category,
|
if ("Настройка".equals(category)
|
||||||
final List<Command> commands,
|
&& event.getMember() != null
|
||||||
final StringSelectInteractionEvent event) {
|
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
|
||||||
String emoji = HelpCategories.getEmoji(category);
|
event.reply("❌ Эта категория доступна только администраторам!").setEphemeral(true).queue();
|
||||||
Color color = HelpCategories.getColor(category);
|
return false;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if ("Супер-Админ".equals(category)
|
||||||
// Добавляем оставшиеся команды
|
&& !config.getSuperAdminId().equals(event.getUser().getId())) {
|
||||||
if (commandList.length() > 0) {
|
event.reply("❌ Эта категория доступна только владельцу бота.").setEphemeral(true).queue();
|
||||||
embed.addField("", commandList.toString().trim(), false);
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
embed.addField(
|
|
||||||
"💡 Подсказка", "Используй селектор ниже для переключения между категориями", false);
|
|
||||||
|
|
||||||
embed.withFooter(
|
|
||||||
"Запросил: " + event.getUser().getName() + " • Всего команд: " + commands.size(),
|
|
||||||
event.getUser().getAvatarUrl());
|
|
||||||
|
|
||||||
return embed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isNsfw(final StringSelectInteractionEvent event) {
|
private static boolean isNsfw(final StringSelectInteractionEvent event) {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
package creator.loki.feature.help.commands;
|
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.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.registry.CommandRegistry;
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
import creator.loki.embed.EmbedFactory;
|
import creator.loki.feature.help.HelpMainPage;
|
||||||
import creator.loki.embed.HelpCategories;
|
import creator.loki.feature.help.HelpMenuBuilder;
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
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.User;
|
||||||
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
||||||
|
|
||||||
public class HelpCommand implements Command {
|
public class HelpCommand implements Command {
|
||||||
private final CommandRegistry registry;
|
private final HelpMenuBuilder menuBuilder;
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
|
|
||||||
public HelpCommand(CommandRegistry registry, BotConfig config) {
|
public HelpCommand(CommandRegistry registry, BotConfig config) {
|
||||||
this.registry = registry;
|
this.menuBuilder = new HelpMenuBuilder(registry, config);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,9 +33,7 @@ public class HelpCommand implements Command {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(final CommandContext context) {
|
public void execute(final CommandContext context) {
|
||||||
var commands = registry.getAllCommands();
|
var commandsByCategory = menuBuilder.groupByCategory();
|
||||||
Map<String, List<Command>> commandsByCategory =
|
|
||||||
commands.stream().collect(Collectors.groupingBy(Command::getCategory));
|
|
||||||
|
|
||||||
User viewer =
|
User viewer =
|
||||||
context.getAuthor() != null
|
context.getAuthor() != null
|
||||||
|
|
@ -52,37 +44,15 @@ public class HelpCommand implements Command {
|
||||||
boolean isSuper = config.getSuperAdminId().equals(viewer.getId());
|
boolean isSuper = config.getSuperAdminId().equals(viewer.getId());
|
||||||
boolean isNsfwChannel = isNsfw(context);
|
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
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(embed.build())
|
.sendMessageEmbeds(
|
||||||
.addComponents(ActionRow.of(menuBuilder.build()))
|
HelpMainPage.build(
|
||||||
|
context, commandsByCategory, menuBuilder, isAdmin, isSuper, isNsfwChannel)
|
||||||
|
.build())
|
||||||
|
.addComponents(
|
||||||
|
ActionRow.of(
|
||||||
|
menuBuilder.buildSelector(commandsByCategory, isAdmin, isSuper, isNsfwChannel)))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,74 +60,6 @@ public class HelpCommand implements Command {
|
||||||
return context.getChannel() instanceof IAgeRestrictedChannel arc && arc.isNSFW();
|
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
|
@Override
|
||||||
public String getCategory() {
|
public String getCategory() {
|
||||||
return CommandCategories.UTILITY;
|
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;
|
package creator.loki.feature.info.commands;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
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 creator.loki.feature.serverstats.ServerStatsService;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.utils.FileUpload;
|
import net.dv8tion.jda.api.utils.FileUpload;
|
||||||
|
|
||||||
public class ServerStatsCommand implements Command {
|
public class ServerStatsCommand implements Command {
|
||||||
|
|
@ -47,7 +52,6 @@ public class ServerStatsCommand implements Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
String arg = context.getArgs().isEmpty() ? "" : context.getArgs().get(0).toLowerCase();
|
String arg = context.getArgs().isEmpty() ? "" : context.getArgs().get(0).toLowerCase();
|
||||||
|
|
||||||
switch (arg) {
|
switch (arg) {
|
||||||
case "csv" -> handleCsv(context, guild);
|
case "csv" -> handleCsv(context, guild);
|
||||||
case "chart" -> handleChart(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) {
|
private void handleEmbed(final CommandContext context, final Guild guild) {
|
||||||
guild.loadMembers().onSuccess(members -> sendStatsEmbed(context, guild, members));
|
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(
|
private void sendStatsEmbed(
|
||||||
final CommandContext context,
|
final CommandContext context, final Guild guild, final List<Member> members) {
|
||||||
final Guild guild,
|
MemberStats stats = ServerStatsEmbedBuilder.countMembers(members);
|
||||||
java.util.List<net.dv8tion.jda.api.entities.Member> members) {
|
|
||||||
var s = countMembers(members);
|
|
||||||
long textChannels = guild.getTextChannels().size();
|
long textChannels = guild.getTextChannels().size();
|
||||||
long voiceChannels = guild.getVoiceChannels().size();
|
long voiceChannels = guild.getVoiceChannels().size();
|
||||||
long categories = guild.getCategories().size();
|
long categories = guild.getCategories().size();
|
||||||
long totalChannels = textChannels + voiceChannels + categories;
|
long totalChannels = textChannels + voiceChannels + categories;
|
||||||
int roles = guild.getRoles().size();
|
String created = guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
|
||||||
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 ownerName =
|
String ownerName =
|
||||||
guild.getOwner() != null
|
guild.getOwner() != null ? guild.getOwner().getUser().getGlobalName() : "неизвестен";
|
||||||
? guild.getOwner().getUser().getGlobalName()
|
|
||||||
: "неизвестен";
|
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(buildStatsEmbed(
|
.sendMessageEmbeds(
|
||||||
guild, s,
|
ServerStatsEmbedBuilder.build(
|
||||||
new ChannelCounts(textChannels, voiceChannels, categories, totalChannels),
|
guild,
|
||||||
new BoostInfo(boostCount, boostTier),
|
stats,
|
||||||
new GuildMeta(roles, emojis, created, ownerName)))
|
new ChannelCounts(textChannels, voiceChannels, categories, totalChannels),
|
||||||
|
new BoostInfo(guild.getBoostCount(), guild.getBoostTier().getKey()),
|
||||||
|
new GuildMeta(
|
||||||
|
guild.getRoles().size(), guild.getEmojis().size(), created, ownerName)))
|
||||||
.queue();
|
.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) {
|
private void handleCsv(final CommandContext context, final Guild guild) {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
package creator.loki.feature.level.commands;
|
package creator.loki.feature.level.commands;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.feature.level.commands.util.LevelRoleFormatter;
|
|
||||||
import creator.loki.feature.level.service.LevelRoleService;
|
import creator.loki.feature.level.service.LevelRoleService;
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
|
||||||
public class LevelRoleCommand implements Command {
|
public class LevelRoleCommand implements Command {
|
||||||
private final LevelRoleService levelRoleService;
|
private final LevelRoleSubcommands subcommands;
|
||||||
private final ConfigRepository configRepository;
|
|
||||||
|
|
||||||
public LevelRoleCommand(LevelRoleService levelRoleService, ConfigRepository configRepository) {
|
public LevelRoleCommand(LevelRoleService levelRoleService, ConfigRepository configRepository) {
|
||||||
this.levelRoleService = levelRoleService;
|
this.subcommands = new LevelRoleSubcommands(levelRoleService, configRepository);
|
||||||
this.configRepository = configRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -56,125 +52,15 @@ public class LevelRoleCommand implements Command {
|
||||||
|
|
||||||
List<String> args = context.getArgs();
|
List<String> args = context.getArgs();
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
context
|
subcommands.showMappings(context);
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
LevelRoleFormatter.buildMappingsEmbed(configRepository, levelRoleService).build())
|
|
||||||
.queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (args.get(0).toLowerCase()) {
|
switch (args.get(0).toLowerCase()) {
|
||||||
case "set" -> handleSet(context, args);
|
case "set" -> subcommands.set(context, args);
|
||||||
case "remove" -> handleRemove(context, args);
|
case "remove" -> subcommands.remove(context, args);
|
||||||
case "clear" -> handleClear(context);
|
case "clear" -> subcommands.clear(context);
|
||||||
default ->
|
default -> subcommands.showMappings(context);
|
||||||
context
|
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
LevelRoleFormatter.buildMappingsEmbed(configRepository, levelRoleService).build())
|
|
||||||
.queue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
package creator.loki.feature.level.commands.admin;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
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.repository.UserStatsRepository;
|
||||||
|
import creator.loki.feature.level.service.LevelResetService;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import net.dv8tion.jda.api.Permission;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/** Одноразовая команда для сброса уровней и XP всех пользователей. */
|
||||||
* Одноразовая команда для сброса уровней и XP всех пользователей Сохраняет время в войсе и
|
|
||||||
* количество сообщений
|
|
||||||
*/
|
|
||||||
public class ResetLevelsCommand implements Command {
|
public class ResetLevelsCommand implements Command {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsCommand.class);
|
private static final Logger logger = LoggerFactory.getLogger(ResetLevelsCommand.class);
|
||||||
private static final String RESET_KEY = "levels_reset_executed";
|
|
||||||
|
|
||||||
private final UserStatsRepository repository;
|
private final LevelResetService resetService;
|
||||||
private final GuildConfigRepository guildConfig;
|
|
||||||
|
|
||||||
public ResetLevelsCommand(UserStatsRepository repository, GuildConfigRepository guildConfig) {
|
public ResetLevelsCommand(UserStatsRepository repository, GuildConfigRepository guildConfig) {
|
||||||
this.repository = repository;
|
this.resetService = new LevelResetService(repository, guildConfig);
|
||||||
this.guildConfig = guildConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -54,104 +43,29 @@ public class ResetLevelsCommand implements Command {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(final CommandContext context) {
|
public void execute(final CommandContext context) {
|
||||||
// Проверка прав администратора
|
|
||||||
if (context.getAuthor() == null
|
if (context.getAuthor() == null
|
||||||
|| !context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
|| !context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||||
context
|
context.getChannel().sendMessageEmbeds(ResetLevelsEmbeds.noPermission()).queue();
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedFactory.create()
|
|
||||||
.withTitle("❌ Недостаточно прав")
|
|
||||||
.withDescription("Эта команда доступна только администраторам.")
|
|
||||||
.withColor(Color.RED)
|
|
||||||
.build())
|
|
||||||
.queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка, была ли команда уже выполнена
|
|
||||||
String guildId = context.getGuild().getId();
|
String guildId = context.getGuild().getId();
|
||||||
String resetValue = guildConfig.get(guildId, RESET_KEY);
|
if (resetService.isAlreadyReset(guildId)) {
|
||||||
boolean alreadyReset = "true".equals(resetValue);
|
context.getChannel().sendMessageEmbeds(ResetLevelsEmbeds.alreadyReset()).queue();
|
||||||
|
|
||||||
if (alreadyReset) {
|
|
||||||
context
|
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedFactory.create()
|
|
||||||
.withTitle("⚠️ Команда уже выполнена")
|
|
||||||
.withDescription(
|
|
||||||
"Сброс уровней уже был выполнен ранее."
|
|
||||||
+ "\nЭта команда одноразовая и не может быть использована повторно.")
|
|
||||||
.withColor(Color.ORANGE)
|
|
||||||
.addField(
|
|
||||||
"💡 Информация",
|
|
||||||
"Если нужно сбросить уровни снова, обратитесь к разработчику бота.",
|
|
||||||
false)
|
|
||||||
.build())
|
|
||||||
.queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем предупреждение с кнопками подтверждения
|
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(ResetLevelsEmbeds.confirmation())
|
||||||
EmbedFactory.create()
|
.addComponents(ResetLevelsEmbeds.confirmationButtons(context.getAuthor().getId()))
|
||||||
.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(), "❌ Отмена")))
|
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Выполняет сброс уровней (вызывается из ButtonHandler) */
|
/** Выполняет сброс уровней (вызывается из ButtonHandler). */
|
||||||
public void executeReset(final String guildId, final String userId) {
|
public void executeReset(final String guildId, final String userId) {
|
||||||
try {
|
try {
|
||||||
LoggerDecorator.warn(logger, "Начало сброса уровней для гильдии: " + guildId);
|
resetService.resetLevels(guildId, userId);
|
||||||
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);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LoggerDecorator.error(logger, "Ошибка при сбросе уровней", e);
|
LoggerDecorator.error(logger, "Ошибка при сбросе уровней", e);
|
||||||
throw new RuntimeException("Ошибка при сбросе уровней: " + e.getMessage(), 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;
|
this.lastVoiceJoin = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserStats(final Builder builder) {
|
UserStats(final UserStatsBuilder builder) {
|
||||||
this.userId = builder.userId;
|
this.userId = builder.userId;
|
||||||
this.level = builder.level;
|
this.level = builder.level;
|
||||||
this.totalXP = builder.totalXP;
|
this.totalXP = builder.totalXP;
|
||||||
|
|
@ -99,62 +99,14 @@ public class UserStats {
|
||||||
userId, level, totalXP, messageCount, voiceMinutes);
|
userId, level, totalXP, messageCount, voiceMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder extends UserStatsBuilder {
|
||||||
private String userId;
|
|
||||||
private int level;
|
|
||||||
private int totalXP;
|
|
||||||
private int messageCount;
|
|
||||||
private long voiceMinutes;
|
|
||||||
private long lastMessageTime;
|
|
||||||
private long lastVoiceJoin;
|
|
||||||
|
|
||||||
public Builder() {}
|
public Builder() {}
|
||||||
|
|
||||||
public Builder(final UserStats existing) {
|
public Builder(final UserStats existing) {
|
||||||
this.userId = existing.userId;
|
super(existing);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public UserStats build() {
|
public UserStats build() {
|
||||||
return new UserStats(this);
|
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;
|
package creator.loki.feature.logging;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.util.GuildChannelResolve;
|
import creator.loki.util.GuildChannelResolve;
|
||||||
import net.dv8tion.jda.api.Permission;
|
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) {
|
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) {
|
if (args.size() < 2) {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessage(
|
.sendMessage(
|
||||||
"❌ Укажите текстовый канал: `#канал`, `<#id>` или id. Пример: `!log-setup moderation 123456789012345678`")
|
"❌ Укажите текстовый канал: `#канал`, `<#id>` или id. Пример: `!log-setup moderation 123456789012345678`")
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
TextChannel channel =
|
TextChannel channel =
|
||||||
GuildChannelResolve.text(context.getGuild(), context.getMessage(), args, 1);
|
GuildChannelResolve.text(context.getGuild(), context.getMessage(), args, 1);
|
||||||
|
|
@ -78,101 +107,7 @@ public class LogSetupPrefixCommand implements Command {
|
||||||
.sendMessage(
|
.sendMessage(
|
||||||
"❌ Не найден текстовый канал. Укажите `#канал`, `<#id>` или id из 17-20 цифр.")
|
"❌ Не найден текстовый канал. Укажите `#канал`, `<#id>` или id из 17-20 цифр.")
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
configRepo.set("moderation.log_channel_id", channel.getId());
|
return channel;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,14 @@
|
||||||
package creator.loki.feature.metrics;
|
package creator.loki.feature.metrics;
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.BindException;
|
import java.net.BindException;
|
||||||
import java.net.InetSocketAddress;
|
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 java.util.concurrent.Executors;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.core.Lifecycle;
|
import creator.loki.core.Lifecycle;
|
||||||
import creator.loki.feature.tiktok.TempFile;
|
|
||||||
import creator.loki.feature.tiktok.TempFileManager;
|
import creator.loki.feature.tiktok.TempFileManager;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -26,16 +19,14 @@ public class HealthHttpServer implements Lifecycle {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
||||||
|
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
private final MetricsCollector metrics;
|
private final HttpEndpoints endpoints;
|
||||||
private final TempFileManager tempFileManager;
|
|
||||||
private HttpServer server;
|
private HttpServer server;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public HealthHttpServer(
|
public HealthHttpServer(
|
||||||
BotConfig config, MetricsCollector metrics, TempFileManager tempFileManager) {
|
BotConfig config, MetricsCollector metrics, TempFileManager tempFileManager) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.metrics = metrics;
|
this.endpoints = new HttpEndpoints(metrics, tempFileManager, logger);
|
||||||
this.tempFileManager = tempFileManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void start() {
|
public synchronized void start() {
|
||||||
|
|
@ -49,8 +40,8 @@ public class HealthHttpServer implements Lifecycle {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
server = bindHealthServer(port);
|
server = bindHealthServer(port);
|
||||||
server.createContext("/health", this::handleHealth);
|
server.createContext("/health", endpoints::handleHealth);
|
||||||
server.createContext("/d/", this::handleDownload);
|
server.createContext("/d/", endpoints::handleDownload);
|
||||||
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||||
server.start();
|
server.start();
|
||||||
int bound = server.getAddress().getPort();
|
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
|
@Override
|
||||||
public synchronized void shutdown() {
|
public synchronized void shutdown() {
|
||||||
if (server != null) {
|
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 {
|
public class MuteCommand implements Command {
|
||||||
private final MuteNotifier notifier;
|
private final MuteNotifier notifier;
|
||||||
private final ConfigRepository configRepo;
|
|
||||||
private final MuteRoleService muteRoleService;
|
private final MuteRoleService muteRoleService;
|
||||||
private final MuteScheduler muteScheduler;
|
private final MuteScheduler muteScheduler;
|
||||||
|
|
||||||
|
|
@ -28,7 +27,6 @@ public class MuteCommand implements Command {
|
||||||
final MuteRoleService mrs,
|
final MuteRoleService mrs,
|
||||||
final MuteScheduler ms) {
|
final MuteScheduler ms) {
|
||||||
this.notifier = new MuteNotifier(c, a, cr, ms);
|
this.notifier = new MuteNotifier(c, a, cr, ms);
|
||||||
this.configRepo = cr;
|
|
||||||
this.muteRoleService = mrs;
|
this.muteRoleService = mrs;
|
||||||
this.muteScheduler = ms;
|
this.muteScheduler = ms;
|
||||||
}
|
}
|
||||||
|
|
@ -42,10 +40,13 @@ public class MuteCommand implements Command {
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return "Выдать мут на время или навсегда";
|
return "Выдать мут на время или навсегда";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsage() {
|
public String getUsage() {
|
||||||
return "mute <@user> <5m|1h|1d|perm> [причина]";
|
return "mute <@user> <5m|1h|1d|perm> [причина]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getCategory() {
|
public String getCategory() {
|
||||||
return CommandCategories.MODERATION;
|
return CommandCategories.MODERATION;
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +72,9 @@ public class MuteCommand implements Command {
|
||||||
if (!validateMute(ctx, target, mod, g)) {
|
if (!validateMute(ctx, target, mod, g)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var parsed = parseDurationAndReason(ctx);
|
var parsed = MuteArgs.parse(ctx.getArgs());
|
||||||
if (parsed == null) {
|
if (parsed == null) {
|
||||||
|
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Role muteRole;
|
Role muteRole;
|
||||||
|
|
@ -86,20 +88,18 @@ public class MuteCommand implements Command {
|
||||||
err(ctx, "Подними роль бота выше роли **Мут**");
|
err(ctx, "Подними роль бота выше роли **Мут**");
|
||||||
return;
|
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) {
|
if (caseId < 0) {
|
||||||
err(ctx, "Ошибка записи кейса");
|
err(ctx, "Ошибка записи кейса");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyMuteRole(
|
applyMuteRole(
|
||||||
new MuteContext(ctx, g, target, muteRole, caseId),
|
new MuteContext(ctx, g, target, muteRole, caseId),
|
||||||
new MuteDetails(parsed.dur, parsed.reason, mod));
|
new MuteDetails(parsed.dur(), parsed.reason(), mod));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateMute(
|
private boolean validateMute(
|
||||||
final CommandContext ctx,
|
final CommandContext ctx, final Member target, final Member mod, final Guild g) {
|
||||||
final Member target,
|
|
||||||
final Member mod,
|
|
||||||
final Guild g) {
|
|
||||||
if (target.getId().equals(mod.getId())) {
|
if (target.getId().equals(mod.getId())) {
|
||||||
err(ctx, "Нельзя замутить себя");
|
err(ctx, "Нельзя замутить себя");
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -115,54 +115,6 @@ public class MuteCommand implements Command {
|
||||||
return true;
|
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(
|
private record MuteContext(
|
||||||
CommandContext ctx, Guild g, Member target, Role muteRole, long caseId) {}
|
CommandContext ctx, Guild g, Member target, Role muteRole, long caseId) {}
|
||||||
|
|
||||||
|
|
@ -178,8 +130,7 @@ public class MuteCommand implements Command {
|
||||||
var mod = md.mod();
|
var mod = md.mod();
|
||||||
var ctx = mc.ctx();
|
var ctx = mc.ctx();
|
||||||
boolean permanent = dur.permanent();
|
boolean permanent = dur.permanent();
|
||||||
long secs = dur.secondsTotal();
|
long untilMs = permanent ? 0L : System.currentTimeMillis() + dur.secondsTotal() * 1000;
|
||||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
|
||||||
g.addRoleToMember(target, muteRole)
|
g.addRoleToMember(target, muteRole)
|
||||||
.queue(
|
.queue(
|
||||||
ok -> {
|
ok -> {
|
||||||
|
|
@ -190,10 +141,9 @@ public class MuteCommand implements Command {
|
||||||
.queue();
|
.queue();
|
||||||
notifier.sendMuteLogEmbed(g, target, mod, caseId, dur, reason, untilMs);
|
notifier.sendMuteLogEmbed(g, target, mod, caseId, dur, reason, untilMs);
|
||||||
},
|
},
|
||||||
err2 -> {
|
err2 -> err(ctx, "Ошибка выдачи роли"));
|
||||||
err(ctx, "Ошибка выдачи роли");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void err(final CommandContext ctx, final String msg) {
|
private static void err(final CommandContext ctx, final String msg) {
|
||||||
ModerationHelper.error(ctx, msg);
|
ModerationHelper.error(ctx, msg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,7 @@ class MuteNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
long createMuteCase(
|
long createMuteCase(
|
||||||
Guild g,
|
Guild g, Member target, Member mod, MuteDurationParser.MuteDuration dur, String reason) {
|
||||||
Member target,
|
|
||||||
Member mod,
|
|
||||||
MuteDurationParser.MuteDuration dur,
|
|
||||||
String reason) {
|
|
||||||
boolean permanent = dur.permanent();
|
boolean permanent = dur.permanent();
|
||||||
long secs = dur.secondsTotal();
|
long secs = dur.secondsTotal();
|
||||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||||
|
|
@ -105,18 +101,11 @@ class MuteNotifier {
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setTitle("🔇 Мут")
|
.setTitle("🔇 Мут")
|
||||||
.setColor(Color.DARK_GRAY)
|
.setColor(Color.DARK_GRAY)
|
||||||
.addField(
|
.addField("Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
||||||
"Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
|
||||||
.addField("Модератор", mod.getAsMention(), false)
|
.addField("Модератор", mod.getAsMention(), false)
|
||||||
.addField("Кейс", "`" + caseId + "`", true)
|
.addField("Кейс", "`" + caseId + "`", true)
|
||||||
.addField(
|
.addField("Срок", permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>", true)
|
||||||
"Срок",
|
.addField("Причина", ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH), false)
|
||||||
permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>",
|
|
||||||
true)
|
|
||||||
.addField(
|
|
||||||
"Причина",
|
|
||||||
ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH),
|
|
||||||
false)
|
|
||||||
.setTimestamp(OffsetDateTime.now())
|
.setTimestamp(OffsetDateTime.now())
|
||||||
.build())
|
.build())
|
||||||
.queue();
|
.queue();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
package creator.loki.feature.moderation.commands.punish;
|
package creator.loki.feature.moderation.commands.punish;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
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.ActiveMuteRepository;
|
||||||
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
||||||
import creator.loki.feature.moderation.util.ModerationHelper;
|
import creator.loki.feature.moderation.util.ModerationHelper;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
|
||||||
public class UnmuteCommand implements Command {
|
public class UnmuteCommand implements Command {
|
||||||
private final ModerationCaseRepository cases;
|
private final ModerationCaseRepository cases;
|
||||||
private final ActiveMuteRepository activeMutes;
|
private final ActiveMuteRepository activeMutes;
|
||||||
private final ConfigRepository configRepo;
|
|
||||||
private final GuildConfigRepository guildConfig;
|
private final GuildConfigRepository guildConfig;
|
||||||
private final MuteScheduler muteScheduler;
|
private final MuteScheduler muteScheduler;
|
||||||
|
private final UnmuteNotifier notifier;
|
||||||
|
|
||||||
public UnmuteCommand(
|
public UnmuteCommand(
|
||||||
final ModerationCaseRepository c,
|
final ModerationCaseRepository c,
|
||||||
|
|
@ -33,9 +28,9 @@ public class UnmuteCommand implements Command {
|
||||||
final MuteScheduler ms) {
|
final MuteScheduler ms) {
|
||||||
cases = c;
|
cases = c;
|
||||||
activeMutes = a;
|
activeMutes = a;
|
||||||
configRepo = cr;
|
|
||||||
guildConfig = gc;
|
guildConfig = gc;
|
||||||
muteScheduler = ms;
|
muteScheduler = ms;
|
||||||
|
notifier = new UnmuteNotifier(cr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -105,65 +100,10 @@ public class UnmuteCommand implements Command {
|
||||||
long caseId = cases.insert(g.getId(), "UNMUTE", t.getId(), mod.getId(), reason, null, null);
|
long caseId = cases.insert(g.getId(), "UNMUTE", t.getId(), mod.getId(), reason, null, null);
|
||||||
g.removeRoleFromMember(t, muteRole)
|
g.removeRoleFromMember(t, muteRole)
|
||||||
.queue(
|
.queue(
|
||||||
ok -> onMuteRemoved(ctx, g, t, mod, caseId, reason),
|
ok -> notifier.notifyMuteRemoved(ctx, g, t, mod, caseId, reason),
|
||||||
err2 -> err(ctx, "Ошибка снятия роли"));
|
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) {
|
private static void err(final CommandContext ctx, final String msg) {
|
||||||
ModerationHelper.error(ctx, 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+ варна";
|
String reason = "Авто-мут: 3+ варна";
|
||||||
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
|
long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000;
|
||||||
long durSec = AUTO_MUTE_HOURS * 3600;
|
long durSec = AUTO_MUTE_HOURS * 3600;
|
||||||
long caseId =
|
long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
|
||||||
cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs);
|
|
||||||
if (caseId < 0) {
|
if (caseId < 0) {
|
||||||
return;
|
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;
|
package creator.loki.feature.moderation.log;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
@ -9,7 +8,6 @@ import creator.loki.core.MessageCache;
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.audit.ActionType;
|
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.entities.Guild;
|
||||||
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
|
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
|
||||||
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
|
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
|
||||||
|
|
@ -52,120 +50,87 @@ public class ModerationLogger extends ListenerAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onMessageDelete(MessageDeleteEvent e) {
|
public void onMessageDelete(MessageDeleteEvent e) {
|
||||||
var content = messageCache.getContent(e.getMessageId());
|
var content = messageCache.getContent(e.getMessageId());
|
||||||
var g = e.getGuild();
|
AuditLogs.sendForTarget(
|
||||||
g.retrieveAuditLogs()
|
e.getGuild(),
|
||||||
.type(ActionType.MESSAGE_DELETE)
|
ActionType.MESSAGE_DELETE,
|
||||||
.limit(5)
|
5,
|
||||||
.queue(
|
e.getMessageId(),
|
||||||
logs -> {
|
entry -> LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content),
|
||||||
var entry =
|
this::send);
|
||||||
logs.stream()
|
|
||||||
.filter(log -> e.getMessageId().equals(log.getTargetId()))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
|
public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
|
||||||
var g = e.getGuild();
|
|
||||||
var size = e.getMessageIds().size();
|
var size = e.getMessageIds().size();
|
||||||
g.retrieveAuditLogs()
|
AuditLogs.sendFirst(
|
||||||
.type(ActionType.MESSAGE_BULK_DELETE)
|
e.getGuild(),
|
||||||
.limit(3)
|
ActionType.MESSAGE_BULK_DELETE,
|
||||||
.queue(
|
3,
|
||||||
logs -> {
|
entry -> LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size),
|
||||||
var entry = logs.stream().findFirst().orElse(null);
|
this::send);
|
||||||
send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildBan(GuildBanEvent ev) {
|
public void onGuildBan(GuildBanEvent ev) {
|
||||||
ev.getGuild()
|
AuditLogs.sendForTarget(
|
||||||
.retrieveAuditLogs()
|
ev.getGuild(),
|
||||||
.type(ActionType.BAN)
|
ActionType.BAN,
|
||||||
.limit(5)
|
5,
|
||||||
.queue(
|
ev.getUser().getId(),
|
||||||
l -> {
|
entry ->
|
||||||
var entry = findEntry(l, ev.getUser().getId());
|
LogEmbeds.ban(
|
||||||
if (entry == null) {
|
ev.getUser().getAsMention(),
|
||||||
return;
|
AuditLogs.mod(entry),
|
||||||
}
|
entry.getReason(),
|
||||||
send(
|
ev.getUser().getAvatarUrl()),
|
||||||
ev.getGuild(),
|
this::send);
|
||||||
LogEmbeds.ban(
|
|
||||||
ev.getUser().getAsMention(),
|
|
||||||
mod(entry),
|
|
||||||
entry.getReason(),
|
|
||||||
ev.getUser().getAvatarUrl()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildUnban(GuildUnbanEvent ev) {
|
public void onGuildUnban(GuildUnbanEvent ev) {
|
||||||
ev.getGuild()
|
AuditLogs.sendForTarget(
|
||||||
.retrieveAuditLogs()
|
ev.getGuild(),
|
||||||
.type(ActionType.UNBAN)
|
ActionType.UNBAN,
|
||||||
.limit(5)
|
5,
|
||||||
.queue(
|
ev.getUser().getId(),
|
||||||
l -> {
|
entry ->
|
||||||
var entry = findEntry(l, ev.getUser().getId());
|
LogEmbeds.unban(
|
||||||
if (entry == null) {
|
ev.getUser().getAsMention(), AuditLogs.mod(entry), ev.getUser().getAvatarUrl()),
|
||||||
return;
|
this::send);
|
||||||
}
|
|
||||||
send(
|
|
||||||
ev.getGuild(),
|
|
||||||
LogEmbeds.unban(
|
|
||||||
ev.getUser().getAsMention(), mod(entry), ev.getUser().getAvatarUrl()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildMemberRemove(GuildMemberRemoveEvent ev) {
|
public void onGuildMemberRemove(GuildMemberRemoveEvent ev) {
|
||||||
ev.getGuild()
|
AuditLogs.sendForTarget(
|
||||||
.retrieveAuditLogs()
|
ev.getGuild(),
|
||||||
.type(ActionType.KICK)
|
ActionType.KICK,
|
||||||
.limit(5)
|
5,
|
||||||
.queue(
|
ev.getUser().getId(),
|
||||||
l -> {
|
entry ->
|
||||||
var entry = findEntry(l, ev.getUser().getId());
|
LogEmbeds.kick(
|
||||||
if (entry == null) {
|
ev.getUser().getAsMention(),
|
||||||
return;
|
AuditLogs.mod(entry),
|
||||||
}
|
entry.getReason(),
|
||||||
send(
|
ev.getUser().getAvatarUrl()),
|
||||||
ev.getGuild(),
|
this::send);
|
||||||
LogEmbeds.kick(
|
|
||||||
ev.getUser().getAsMention(),
|
|
||||||
mod(entry),
|
|
||||||
entry.getReason(),
|
|
||||||
ev.getUser().getAvatarUrl()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent e) {
|
public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent e) {
|
||||||
var muted = e.getNewTimeOutEnd() != null;
|
var muted = e.getNewTimeOutEnd() != null;
|
||||||
e.getGuild()
|
AuditLogs.sendForTarget(
|
||||||
.retrieveAuditLogs()
|
e.getGuild(),
|
||||||
.type(ActionType.MEMBER_UPDATE)
|
ActionType.MEMBER_UPDATE,
|
||||||
.limit(5)
|
5,
|
||||||
.queue(
|
e.getUser().getId(),
|
||||||
l -> {
|
entry ->
|
||||||
var entry = findEntry(l, e.getUser().getId());
|
LogEmbeds.timeout(
|
||||||
if (entry == null) {
|
e.getUser().getAsMention(),
|
||||||
return;
|
AuditLogs.mod(entry),
|
||||||
}
|
muted,
|
||||||
send(
|
muted ? e.getNewTimeOutEnd().toEpochSecond() : null,
|
||||||
e.getGuild(),
|
entry.getReason()),
|
||||||
LogEmbeds.timeout(
|
this::send);
|
||||||
e.getUser().getAsMention(),
|
|
||||||
mod(entry),
|
|
||||||
muted,
|
|
||||||
muted ? e.getNewTimeOutEnd().toEpochSecond() : null,
|
|
||||||
entry.getReason()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -177,16 +142,4 @@ public class ModerationLogger extends ListenerAdapter {
|
||||||
public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent e) {
|
public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent e) {
|
||||||
voiceLogger.onGuildVoiceGuildDeafen(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) {
|
private EmbedBuilder voiceMute(GuildVoiceGuildMuteEvent e, boolean muted, List<AuditLogEntry> l) {
|
||||||
var member = e.getMember();
|
var member = e.getMember();
|
||||||
var entry = ModerationLogger.findEntry(l, member.getId(), "mute");
|
var entry = AuditLogs.findEntry(l, member.getId(), "mute");
|
||||||
return LogEmbeds.voiceMute(
|
return LogEmbeds.voiceMute(
|
||||||
member.getAsMention(),
|
member.getAsMention(),
|
||||||
e.getVoiceState().getChannel().getAsMention(),
|
e.getVoiceState().getChannel().getAsMention(),
|
||||||
muted,
|
muted,
|
||||||
entry != null ? ModerationLogger.mod(entry) : null);
|
entry != null ? AuditLogs.mod(entry) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EmbedBuilder voiceDeafen(
|
private EmbedBuilder voiceDeafen(
|
||||||
GuildVoiceGuildDeafenEvent e, boolean deaf, List<AuditLogEntry> l) {
|
GuildVoiceGuildDeafenEvent e, boolean deaf, List<AuditLogEntry> l) {
|
||||||
var member = e.getMember();
|
var member = e.getMember();
|
||||||
var entry = ModerationLogger.findEntry(l, member.getId(), "deaf");
|
var entry = AuditLogs.findEntry(l, member.getId(), "deaf");
|
||||||
return LogEmbeds.voiceDeafen(
|
return LogEmbeds.voiceDeafen(
|
||||||
member.getAsMention(),
|
member.getAsMention(),
|
||||||
e.getVoiceState().getChannel().getAsMention(),
|
e.getVoiceState().getChannel().getAsMention(),
|
||||||
deaf,
|
deaf,
|
||||||
entry != null ? ModerationLogger.mod(entry) : null);
|
entry != null ? AuditLogs.mod(entry) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handle(
|
private void handle(
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,23 @@ public class MusicSlashCommands extends ListenerAdapter {
|
||||||
this.playHandler = new PlaySlashHandler(playerManager);
|
this.playHandler = new PlaySlashHandler(playerManager);
|
||||||
this.queueHandler = new QueueSlashHandler(playerManager);
|
this.queueHandler = new QueueSlashHandler(playerManager);
|
||||||
this.filterHandler = new FilterSlashHandler(playerManager);
|
this.filterHandler = new FilterSlashHandler(playerManager);
|
||||||
this.commandMap = Map.ofEntries(
|
this.commandMap =
|
||||||
Map.entry("play", playHandler::handle),
|
Map.ofEntries(
|
||||||
Map.entry("stop", queueHandler::handleStop),
|
Map.entry("play", playHandler::handle),
|
||||||
Map.entry("skip", queueHandler::handleSkip),
|
Map.entry("stop", queueHandler::handleStop),
|
||||||
Map.entry("queue", queueHandler::handleQueue),
|
Map.entry("skip", queueHandler::handleSkip),
|
||||||
Map.entry("nowplaying", queueHandler::handleNowPlaying),
|
Map.entry("queue", queueHandler::handleQueue),
|
||||||
Map.entry("volume", queueHandler::handleVolume),
|
Map.entry("nowplaying", queueHandler::handleNowPlaying),
|
||||||
Map.entry("pause", queueHandler::handlePause),
|
Map.entry("volume", queueHandler::handleVolume),
|
||||||
Map.entry("resume", queueHandler::handleResume),
|
Map.entry("pause", queueHandler::handlePause),
|
||||||
Map.entry("shuffle", queueHandler::handleShuffle),
|
Map.entry("resume", queueHandler::handleResume),
|
||||||
Map.entry("repeat", queueHandler::handleRepeat),
|
Map.entry("shuffle", queueHandler::handleShuffle),
|
||||||
Map.entry("bassboost", filterHandler::handleBassboost),
|
Map.entry("repeat", queueHandler::handleRepeat),
|
||||||
Map.entry("nightcore", filterHandler::handleNightcore),
|
Map.entry("bassboost", filterHandler::handleBassboost),
|
||||||
Map.entry("vaporwave", filterHandler::handleVaporwave),
|
Map.entry("nightcore", filterHandler::handleNightcore),
|
||||||
Map.entry("equalizer", filterHandler::handleEqualizer),
|
Map.entry("vaporwave", filterHandler::handleVaporwave),
|
||||||
Map.entry("reset", filterHandler::handleReset));
|
Map.entry("equalizer", filterHandler::handleEqualizer),
|
||||||
|
Map.entry("reset", filterHandler::handleReset));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
package creator.loki.feature.music.commands;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
|
|
@ -30,11 +29,11 @@ public class FilterCommands {
|
||||||
return new BandCommand(pm, 2);
|
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());
|
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()) {
|
if (!m.getLink().isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -43,145 +42,4 @@ public class FilterCommands {
|
||||||
.queue();
|
.queue();
|
||||||
return false;
|
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;
|
package creator.loki.feature.music.commands;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
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.core.PlayerManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
|
||||||
|
|
||||||
public class PlayerCommands {
|
public class PlayerCommands {
|
||||||
|
|
||||||
private PlayerCommands() {}
|
private PlayerCommands() {}
|
||||||
|
|
||||||
public static Command skip(PlayerManager playerManager) {
|
public static Command skip(PlayerManager playerManager) {
|
||||||
|
|
@ -22,145 +17,4 @@ public class PlayerCommands {
|
||||||
public static Command leave(PlayerManager playerManager) {
|
public static Command leave(PlayerManager playerManager) {
|
||||||
return new LeaveCommand(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.error;
|
||||||
import static creator.loki.embed.EmbedTemplates.info;
|
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.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class QueueCommands {
|
public class QueueCommands {
|
||||||
private static final Color C = new Color(88, 101, 242);
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
|
private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class);
|
||||||
|
|
||||||
private QueueCommands() {}
|
private QueueCommands() {}
|
||||||
|
|
||||||
public static Command queue(final PlayerManager pm) {
|
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) {
|
public static Command nowplaying(final PlayerManager pm) {
|
||||||
BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeNowPlaying;
|
return new SimpleCommand(
|
||||||
return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec);
|
pm,
|
||||||
|
"nowplaying",
|
||||||
|
"Показать текущий трек",
|
||||||
|
"nowplaying",
|
||||||
|
"Музыка",
|
||||||
|
QueueCommands::executeNowPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Command volume(final PlayerManager pm) {
|
public static Command volume(final PlayerManager pm) {
|
||||||
BiConsumer<PlayerManager, CommandContext> exec = QueueCommands::executeVolume;
|
return new SimpleCommand(
|
||||||
return new Mc(pm, "volume", "Регулировка громкости (0-100)", "volume [0-100]", "Музыка", exec);
|
pm,
|
||||||
}
|
"volume",
|
||||||
|
"Регулировка громкости (0-100)",
|
||||||
private static final class Mc implements Command {
|
"volume [0-100]",
|
||||||
private final PlayerManager p;
|
"Музыка",
|
||||||
private final String n;
|
QueueCommands::executeVolume);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void executeQueue(final PlayerManager pm, final CommandContext ctx) {
|
private static void executeQueue(final PlayerManager pm, final CommandContext ctx) {
|
||||||
var m = pm.getMusicManager(ctx.getGuild().getIdLong());
|
var embed = QueueView.queue(pm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler());
|
||||||
var cur = m.getScheduler().getCurrentTrack();
|
if (embed == null) {
|
||||||
var q = m.getScheduler().getQueue();
|
|
||||||
if (cur == null && q.isEmpty()) {
|
|
||||||
ctx.getChannel().sendMessage("Очередь пуста.").queue();
|
ctx.getChannel().sendMessage("Очередь пуста.").queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var embed = EmbedFactory.create().withTitle("Очередь треков").withColor(C);
|
ctx.getChannel().sendMessageEmbeds(embed).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void executeNowPlaying(final PlayerManager pm, final CommandContext ctx) {
|
private static void executeNowPlaying(final PlayerManager pm, final CommandContext ctx) {
|
||||||
var lm = pm.getLavalinkManager();
|
var guildId = ctx.getGuild().getIdLong();
|
||||||
var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack();
|
var link = pm.getLavalinkManager().getClient().getLinkIfCached(guildId);
|
||||||
if (track == null) {
|
var embed = QueueView.nowPlaying(pm.getMusicManager(guildId).getScheduler(), link);
|
||||||
|
if (embed == null) {
|
||||||
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
|
ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var l = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong());
|
ctx.getChannel().sendMessageEmbeds(embed).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void executeVolume(final PlayerManager pm, final CommandContext ctx) {
|
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()) {
|
if (ctx.getArgs().isEmpty()) {
|
||||||
var v = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getVolume() : 100;
|
ctx.getChannel()
|
||||||
ctx.getChannel().sendMessageEmbeds(info("Громкость", "Текущая: **" + v + "%**")).queue();
|
.sendMessageEmbeds(info("Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"))
|
||||||
|
.queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0))));
|
var vol = VolumeControl.clamp(Integer.parseInt(ctx.getArgs().get(0)));
|
||||||
if (l != null && l.getCachedPlayer() != null) {
|
VolumeControl.set(link, vol, err -> LOG.warn("Lavalink: {}", err.getMessage()));
|
||||||
l.createOrUpdatePlayer()
|
|
||||||
.setVolume(vol)
|
|
||||||
.subscribe(null, err -> LOG.warn("Lavalink: {}", err.getMessage()));
|
|
||||||
}
|
|
||||||
ctx.getChannel()
|
ctx.getChannel()
|
||||||
.sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**"))
|
.sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**"))
|
||||||
.queue();
|
.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.LavalinkClient;
|
||||||
import dev.arbjerg.lavalink.client.Link;
|
import dev.arbjerg.lavalink.client.Link;
|
||||||
import dev.arbjerg.lavalink.client.NodeOptions;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -58,57 +54,12 @@ public class LavalinkManager {
|
||||||
.setHttpTimeout(30000L)
|
.setHttpTimeout(30000L)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
registerListeners();
|
new LavalinkEventRouter(client, musicManagers).register();
|
||||||
|
|
||||||
LoggerDecorator.music(
|
LoggerDecorator.music(
|
||||||
logger, "LavalinkManager инициализирован, нода: " + lavalinkConfig.getWsUri());
|
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() {
|
public LavalinkClient getClient() {
|
||||||
return client;
|
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;
|
package creator.loki.feature.music.listener;
|
||||||
|
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
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.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
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) {
|
public void handleSkip(final MessageChannel ch, final Guild guild, final Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||||
if (s.getCurrentTrack() == null) {
|
if (s.getCurrentTrack() == null) {
|
||||||
send(ch, err("Нечего пропускать."));
|
send(ch, MusicEmbeds.err("Нечего пропускать."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var t = s.getCurrentTrack().getInfo().getTitle();
|
var t = s.getCurrentTrack().getInfo().getTitle();
|
||||||
s.skip();
|
s.skip();
|
||||||
send(ch, ok("⏭ Пропущено", "**" + t + "**"));
|
send(ch, MusicEmbeds.ok("⏭ Пропущено", "**" + t + "**"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleStop(final MessageChannel ch, final Guild guild, final Member member) {
|
public void handleStop(final MessageChannel ch, final Guild guild, final Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
GuildAudioActions.stopAndDisconnect(lavalinkManager.getMusicManager(guild.getIdLong()), guild);
|
||||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
send(ch, MusicEmbeds.ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
||||||
send(ch, ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleQueue(final MessageChannel ch, final Guild guild) {
|
public void handleQueue(final MessageChannel ch, final Guild guild) {
|
||||||
|
|
@ -49,7 +51,7 @@ public class MusicBotControlHandler {
|
||||||
var cur = s.getCurrentTrack();
|
var cur = s.getCurrentTrack();
|
||||||
var q = s.getQueue();
|
var q = s.getQueue();
|
||||||
if (cur == null && q.isEmpty()) {
|
if (cur == null && q.isEmpty()) {
|
||||||
send(ch, err("Очередь пуста."));
|
send(ch, MusicEmbeds.err("Очередь пуста."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
@ -69,31 +71,31 @@ public class MusicBotControlHandler {
|
||||||
sb.append("... и ещё **").append(q.size() - show).append("** треков");
|
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) {
|
public void handleNowPlaying(final MessageChannel ch, final Guild guild) {
|
||||||
var t = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().getCurrentTrack();
|
var t = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().getCurrentTrack();
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
send(ch, err("Сейчас ничего не играет."));
|
send(ch, MusicEmbeds.err("Сейчас ничего не играет."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
send(
|
send(
|
||||||
ch,
|
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) {
|
public void handleLeave(final MessageChannel ch, final Guild guild, final Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lavalinkManager.getMusicManager(guild.getIdLong()).stop();
|
GuildAudioActions.stopAndDisconnect(lavalinkManager.getMusicManager(guild.getIdLong()), guild);
|
||||||
guild.getJDA().getDirectAudioController().disconnect(guild);
|
send(ch, MusicEmbeds.ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
|
||||||
send(ch, ok(ERR, "👋 Отключился", "Вышел из голосового канала."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleRepeat(final MessageChannel ch, final Guild guild, final Member member) {
|
public void handleRepeat(final MessageChannel ch, final Guild guild, final Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||||
|
|
@ -101,45 +103,25 @@ public class MusicBotControlHandler {
|
||||||
s.setRepeating(now);
|
s.setRepeating(now);
|
||||||
send(
|
send(
|
||||||
ch,
|
ch,
|
||||||
ok(
|
MusicEmbeds.ok(
|
||||||
now ? "🔁 Повтор включён" : "➡ Повтор выключен",
|
now ? "🔁 Повтор включён" : "➡ Повтор выключен",
|
||||||
now ? "Очередь будет повторяться." : "Повтор отключён."));
|
now ? "Очередь будет повторяться." : "Повтор отключён."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleShuffle(final MessageChannel ch, final Guild guild, final Member member) {
|
public void handleShuffle(final MessageChannel ch, final Guild guild, final Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||||
if (s.getQueue().isEmpty()) {
|
if (s.getQueue().isEmpty()) {
|
||||||
send(ch, err("Очередь пуста."));
|
send(ch, MusicEmbeds.err("Очередь пуста."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.shuffle();
|
s.shuffle();
|
||||||
send(ch, ok("🔀 Перемешано", "Очередь перемешана."));
|
send(ch, MusicEmbeds.ok("🔀 Перемешано", "Очередь перемешана."));
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||||
ch.sendMessageEmbeds(eb.build()).queue();
|
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;
|
package creator.loki.feature.music.listener;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.repository.QueueRepository;
|
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.events.message.MessageReceivedEvent;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -17,58 +13,15 @@ import org.jetbrains.annotations.NotNull;
|
||||||
public class MusicBotListener extends ListenerAdapter {
|
public class MusicBotListener extends ListenerAdapter {
|
||||||
private static final String PREFIX = "m!";
|
private static final String PREFIX = "m!";
|
||||||
|
|
||||||
private final MusicBotPlayHandler playHandler;
|
|
||||||
private final MusicBotControlHandler controlHandler;
|
|
||||||
private final Map<String, Consumer<MessageReceivedEvent>> commandMap;
|
private final Map<String, Consumer<MessageReceivedEvent>> commandMap;
|
||||||
|
|
||||||
public MusicBotListener(
|
public MusicBotListener(
|
||||||
final LavalinkManager lavalinkManager,
|
final LavalinkManager lavalinkManager,
|
||||||
final QueueRepository queueRepository,
|
final QueueRepository queueRepository,
|
||||||
final creator.loki.feature.music.core.PlayerManager playerManager) {
|
final PlayerManager playerManager) {
|
||||||
this.playHandler = new MusicBotPlayHandler(lavalinkManager);
|
var playHandler = new MusicBotPlayHandler(lavalinkManager);
|
||||||
this.controlHandler = new MusicBotControlHandler(lavalinkManager);
|
var controlHandler = new MusicBotControlHandler(lavalinkManager);
|
||||||
this.commandMap = buildCommandMap();
|
this.commandMap = MusicCommandMap.build(playHandler, controlHandler);
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -81,43 +34,13 @@ public class MusicBotListener extends ListenerAdapter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String[] parts = content.substring(PREFIX.length()).trim().split("\\s+");
|
String[] parts = content.substring(PREFIX.length()).trim().split("\\s+");
|
||||||
if (parts.length == 0) {
|
if (parts.length == 0 || event.getGuild() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String cmd = parts[0].toLowerCase();
|
String cmd = parts[0].toLowerCase();
|
||||||
Guild guild = event.getGuild();
|
|
||||||
if (guild == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Consumer<MessageReceivedEvent> handler = commandMap.get(cmd);
|
Consumer<MessageReceivedEvent> handler = commandMap.get(cmd);
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.accept(event);
|
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.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||||
import creator.loki.feature.music.play.TrackLoader;
|
import creator.loki.feature.music.play.TrackLoader;
|
||||||
import dev.arbjerg.lavalink.client.Link;
|
import creator.loki.feature.music.util.MusicEmbeds;
|
||||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
import creator.loki.feature.music.util.TrackQuery;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import creator.loki.feature.music.util.VoiceCheck;
|
||||||
|
import creator.loki.feature.utility.VoiceHelper;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
@ -24,158 +25,21 @@ public class MusicBotPlayHandler {
|
||||||
|
|
||||||
public void handlePlay(
|
public void handlePlay(
|
||||||
final MessageChannel ch, final Guild guild, Member member, final List<String> args) {
|
final MessageChannel ch, final Guild guild, Member member, final List<String> args) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
send(ch, err("Укажи URL или запрос: `m!play <название>`"));
|
send(ch, MusicEmbeds.err("Укажи URL или запрос: `m!play <название>`"));
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Thread.startVirtualThread(() -> connect(guild, member, lavalinkManager)).join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String query = String.join(" ", args);
|
String query = String.join(" ", args);
|
||||||
GuildMusicManager m = mgr(guild);
|
VoiceHelper.connectForPlayback(guild, member, lavalinkManager);
|
||||||
String id = resolveIdentifier(query);
|
GuildMusicManager m = lavalinkManager.getMusicManager(guild.getIdLong());
|
||||||
|
String id = TrackQuery.build(null, query);
|
||||||
trackLoader.loadItem(
|
trackLoader.loadItem(guild.getIdLong(), id, new MessagePlayResponder(ch, m, query));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||||
ch.sendMessageEmbeds(eb.build()).queue();
|
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 java.util.List;
|
||||||
|
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
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.EmbedBuilder;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
@ -12,7 +15,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
class MusicFilterHandler {
|
class MusicFilterHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MusicFilterHandler.class);
|
private static final Logger log = LoggerFactory.getLogger(MusicFilterHandler.class);
|
||||||
private static final int ERR = 0xFF4444;
|
|
||||||
|
|
||||||
private final LavalinkManager lavalinkManager;
|
private final LavalinkManager lavalinkManager;
|
||||||
|
|
||||||
|
|
@ -21,37 +23,25 @@ class MusicFilterHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleVolume(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
void handleVolume(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong());
|
var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong());
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
send(
|
send(ch, MusicEmbeds.ok("🔊 Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"));
|
||||||
ch,
|
|
||||||
ok(
|
|
||||||
"🔊 Громкость",
|
|
||||||
"Текущая: **"
|
|
||||||
+ (link != null && link.getCachedPlayer() != null
|
|
||||||
? link.getCachedPlayer().getVolume()
|
|
||||||
: 100)
|
|
||||||
+ "%**"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
var vol = VolumeControl.clamp(Integer.parseInt(args.get(0)));
|
||||||
if (link != null) {
|
VolumeControl.set(link, vol, err -> log.warn("Lavalink: {}", err.getMessage()));
|
||||||
link.createOrUpdatePlayer()
|
send(ch, MusicEmbeds.ok("🔊 Громкость", "Установлена **" + vol + "%**"));
|
||||||
.setVolume(vol)
|
|
||||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
|
|
||||||
}
|
|
||||||
send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**"));
|
|
||||||
} catch (NumberFormatException e) {
|
} 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) {
|
void handleBassboost(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var level = 10;
|
var level = 10;
|
||||||
|
|
@ -62,43 +52,43 @@ class MusicFilterHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level);
|
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) {
|
void handleNightcore(MessageChannel ch, Guild guild, Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||||
var now = !s.filterState.isNightcore();
|
var now = !s.filterState.isNightcore();
|
||||||
s.setNightcore(now);
|
s.setNightcore(now);
|
||||||
send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
|
send(ch, MusicEmbeds.ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleVaporwave(MessageChannel ch, Guild guild, Member member) {
|
void handleVaporwave(MessageChannel ch, Guild guild, Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler();
|
||||||
var now = !s.filterState.isVaporwave();
|
var now = !s.filterState.isVaporwave();
|
||||||
s.setVaporwave(now);
|
s.setVaporwave(now);
|
||||||
send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
|
send(ch, MusicEmbeds.ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleReset(MessageChannel ch, Guild guild, Member member) {
|
void handleReset(MessageChannel ch, Guild guild, Member member) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().resetFilters();
|
lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().resetFilters();
|
||||||
send(ch, ok("🔄 Сброс", "Фильтры сброшены."));
|
send(ch, MusicEmbeds.ok("🔄 Сброс", "Фильтры сброшены."));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleEqualizer(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
void handleEqualizer(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||||
if (reqVoice(ch, member) == null) {
|
if (VoiceCheck.requireChannel(ch, member) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (args.size() < 2) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -110,31 +100,14 @@ class MusicFilterHandler {
|
||||||
.setEqualizerBand(band, gain);
|
.setEqualizerBand(band, gain);
|
||||||
send(
|
send(
|
||||||
ch,
|
ch,
|
||||||
ok(
|
MusicEmbeds.ok(
|
||||||
"🎛 Эквалайзер",
|
"🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
|
||||||
"Band **" + band + "** → **" + String.format("%.2f", gain) + "**"));
|
|
||||||
} catch (NumberFormatException e) {
|
} 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) {
|
private void send(final MessageChannel ch, final EmbedBuilder eb) {
|
||||||
ch.sendMessageEmbeds(eb.build()).queue();
|
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;
|
package creator.loki.feature.music.manage;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.LongConsumer;
|
import java.util.function.LongConsumer;
|
||||||
|
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.repository.QueueRepository;
|
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.Link;
|
||||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
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 {
|
public class GuildMusicManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GuildMusicManager.class);
|
|
||||||
private final long guildId;
|
private final long guildId;
|
||||||
private final LavalinkManager lavalinkManager;
|
private final LavalinkManager lavalinkManager;
|
||||||
private TrackScheduler scheduler;
|
|
||||||
private final QueueRepository queueRepository;
|
|
||||||
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||||
private ScheduledFuture<?> autoLeaveFuture;
|
private final AutoLeaveController autoLeave;
|
||||||
private final LongConsumer voiceDisconnectHandler;
|
private final QueueStore queueStore;
|
||||||
|
private TrackScheduler scheduler;
|
||||||
|
|
||||||
public GuildMusicManager(
|
public GuildMusicManager(
|
||||||
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
|
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
|
||||||
|
|
@ -42,14 +31,14 @@ public class GuildMusicManager {
|
||||||
LongConsumer voiceDisconnectHandler) {
|
LongConsumer voiceDisconnectHandler) {
|
||||||
this.guildId = guildId;
|
this.guildId = guildId;
|
||||||
this.lavalinkManager = lavalinkManager;
|
this.lavalinkManager = lavalinkManager;
|
||||||
this.queueRepository = queueRepository;
|
this.autoLeave = new AutoLeaveController(this, voiceDisconnectHandler);
|
||||||
this.voiceDisconnectHandler = voiceDisconnectHandler;
|
this.queueStore = new QueueStore(this, lavalinkManager, queueRepository);
|
||||||
this.scheduler = null;
|
this.scheduler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
this.scheduler = new TrackScheduler(this);
|
this.scheduler = new TrackScheduler(this);
|
||||||
loadQueueAsync();
|
queueStore.loadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackScheduler getScheduler() {
|
public TrackScheduler getScheduler() {
|
||||||
|
|
@ -65,7 +54,7 @@ public class GuildMusicManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
cancelAutoLeaveTimer();
|
autoLeave.cancel();
|
||||||
scheduler.stop();
|
scheduler.stop();
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
try {
|
try {
|
||||||
|
|
@ -78,28 +67,11 @@ public class GuildMusicManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startAutoLeaveTimer() {
|
public void startAutoLeaveTimer() {
|
||||||
cancelAutoLeaveTimer();
|
autoLeave.start();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelAutoLeaveTimer() {
|
public void cancelAutoLeaveTimer() {
|
||||||
if (autoLeaveFuture != null && !autoLeaveFuture.isDone()) {
|
autoLeave.cancel();
|
||||||
autoLeaveFuture.cancel(false);
|
|
||||||
autoLeaveFuture = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Link> getLink() {
|
public Optional<Link> getLink() {
|
||||||
|
|
@ -118,77 +90,11 @@ public class GuildMusicManager {
|
||||||
return guildId;
|
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() {
|
void saveQueueDebounced() {
|
||||||
if (saveDebounced != null && !saveDebounced.isDone()) {
|
queueStore.saveDebounced();
|
||||||
saveDebounced.cancel(false);
|
|
||||||
}
|
|
||||||
saveDebounced = executor.schedule(this::saveQueue, 3, TimeUnit.SECONDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveQueue() {
|
void saveQueue() {
|
||||||
if (queueRepository == null) {
|
queueStore.save();
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.Queue;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import creator.loki.util.LoggerDecorator;
|
|
||||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
||||||
|
|
@ -17,8 +16,10 @@ import org.slf4j.LoggerFactory;
|
||||||
public class TrackScheduler {
|
public class TrackScheduler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
||||||
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
||||||
|
|
||||||
private final GuildMusicManager manager;
|
private final GuildMusicManager manager;
|
||||||
private final TrackStarter trackStarter;
|
private final TrackStarter trackStarter;
|
||||||
|
private final TrackEndHandler trackEndHandler;
|
||||||
final Queue<Track> queue = new LinkedList<>();
|
final Queue<Track> queue = new LinkedList<>();
|
||||||
private boolean repeating;
|
private boolean repeating;
|
||||||
public final FilterState filterState = new FilterState();
|
public final FilterState filterState = new FilterState();
|
||||||
|
|
@ -27,6 +28,7 @@ public class TrackScheduler {
|
||||||
public TrackScheduler(GuildMusicManager manager) {
|
public TrackScheduler(GuildMusicManager manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.trackStarter = new TrackStarter(manager.getExecutor());
|
this.trackStarter = new TrackStarter(manager.getExecutor());
|
||||||
|
this.trackEndHandler = new TrackEndHandler(this, manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnLoadFailedNotify(Consumer<String> onLoadFailedNotify) {
|
public void setOnLoadFailedNotify(Consumer<String> onLoadFailedNotify) {
|
||||||
|
|
@ -109,15 +111,7 @@ public class TrackScheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onTrackEnd(final Track lastTrack, AudioTrackEndReason endReason) {
|
public void onTrackEnd(final Track lastTrack, AudioTrackEndReason endReason) {
|
||||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
trackEndHandler.onTrackEnd(lastTrack, endReason);
|
||||||
handleLoadFailed();
|
|
||||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
|
||||||
handleUnexpectedEnd(endReason);
|
|
||||||
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
|
||||||
handleFinished(lastTrack);
|
|
||||||
} else {
|
|
||||||
nextOrAutoLeave();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void startTrack(final Track track) {
|
void startTrack(final Track track) {
|
||||||
|
|
@ -126,29 +120,13 @@ public class TrackScheduler {
|
||||||
trackStarter.startWithRetry(link, track);
|
trackStarter.startWithRetry(link, track);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLoadFailed() {
|
void notifyLoadFailed() {
|
||||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
|
||||||
if (onLoadFailedNotify != null) {
|
if (onLoadFailedNotify != null) {
|
||||||
onLoadFailedNotify.accept("⚠️ Ошибка загрузки трека, пропускаю...");
|
onLoadFailedNotify.accept("⚠️ Ошибка загрузки трека, пропускаю...");
|
||||||
}
|
}
|
||||||
nextOrAutoLeave();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleUnexpectedEnd(final AudioTrackEndReason endReason) {
|
Track pollNext() {
|
||||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
return queue.poll();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
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 creator.loki.feature.utility.VoiceHelper;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
@ -18,6 +20,7 @@ import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
public class PlayCommand implements Command {
|
public class PlayCommand implements Command {
|
||||||
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
|
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
|
||||||
|
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
private final SearchHandler searchHandler;
|
private final SearchHandler searchHandler;
|
||||||
|
|
||||||
|
|
@ -67,23 +70,12 @@ public class PlayCommand implements Command {
|
||||||
handlePickResult(ctx, first);
|
handlePickResult(ctx, first);
|
||||||
return;
|
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();
|
var lm = playerManager.getLavalinkManager();
|
||||||
if (!VoiceHelper.connectForPlayback(guild, member, lm)) {
|
if (!VoiceHelper.connectForPlayback(guild, member, lm)) {
|
||||||
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
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) {
|
private void handlePickResult(final CommandContext ctx, final String first) {
|
||||||
|
|
@ -93,10 +85,15 @@ public class PlayCommand implements Command {
|
||||||
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!resolveVoiceConnection(ctx)) {
|
if (!VoiceHelper.connectForPlayback(
|
||||||
|
guild, ctx.getAuthor(), playerManager.getLavalinkManager())) {
|
||||||
|
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enqueueAndNotify(ctx, t);
|
QueueNotifier.enqueueAndNotify(
|
||||||
|
playerManager.getLavalinkManager().getMusicManager(guild.getIdLong()).getScheduler(),
|
||||||
|
ctx.getChannel(),
|
||||||
|
t);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePlayQuery(
|
private void handlePlayQuery(
|
||||||
|
|
@ -111,53 +108,11 @@ public class PlayCommand implements Command {
|
||||||
sendErr(ctx, "Укажи запрос после источника.");
|
sendErr(ctx, "Укажи запрос после источника.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
boolean isUrl = TrackQuery.isUrl(raw);
|
||||||
String query = isUrl ? null : raw;
|
String id = TrackQuery.build(first, raw);
|
||||||
String id = buildQuery(first, raw);
|
lm.getOrCreateLink(ctx.getGuild().getIdLong())
|
||||||
loadAndHandle(
|
.loadItem(id)
|
||||||
new PlayContext(ctx, lm, ctx.getGuild().getIdLong(), mm),
|
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, isUrl ? null : raw, raw));
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sendErr(final CommandContext ctx, final String msg) {
|
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 java.util.List;
|
||||||
|
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
|
import creator.loki.feature.music.util.TrackFormatter;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
|
||||||
|
|
@ -68,13 +69,6 @@ public class SearchHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatDuration(final long millis) {
|
public static String formatDuration(final long millis) {
|
||||||
long totalSec = millis / 1000;
|
return TrackFormatter.formatDuration(millis);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
package creator.loki.feature.music.slash;
|
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.core.PlayerManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||||
import creator.loki.feature.music.play.TrackLoader;
|
import creator.loki.feature.music.play.TrackLoader;
|
||||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
import creator.loki.feature.music.util.TrackQuery;
|
||||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
import creator.loki.feature.music.util.VoiceCheck;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||||
|
|
||||||
public class PlaySlashHandler {
|
public class PlaySlashHandler {
|
||||||
|
|
@ -23,138 +18,24 @@ public class PlaySlashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handle(final SlashCommandInteractionEvent event) {
|
public void handle(final SlashCommandInteractionEvent event) {
|
||||||
Member member = requireVoiceChannel(event);
|
if (VoiceCheck.requireChannel(event) == null) {
|
||||||
if (member == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String query = event.getOption("query").getAsString();
|
String query = event.getOption("query").getAsString();
|
||||||
Guild guild = event.getGuild();
|
Guild guild = event.getGuild();
|
||||||
event.deferReply().queue();
|
event.deferReply().queue();
|
||||||
if (!guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) {
|
if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
|
||||||
guild
|
guild
|
||||||
.getJDA()
|
.getJDA()
|
||||||
.getDirectAudioController()
|
.getDirectAudioController()
|
||||||
.connect(member.getVoiceState().getChannel().asVoiceChannel());
|
.connect(event.getMember().getVoiceState().getChannel().asVoiceChannel());
|
||||||
}
|
}
|
||||||
GuildMusicManager m = playerManager.getMusicManager(guild.getIdLong());
|
GuildMusicManager m = playerManager.getMusicManager(guild.getIdLong());
|
||||||
if (query.startsWith("http://") || query.startsWith("https://")) {
|
boolean isUrl = TrackQuery.isUrl(query);
|
||||||
loadUrl(event, query, m, guild);
|
String searchQuery = isUrl ? null : query;
|
||||||
} else {
|
trackLoader.loadItem(
|
||||||
search(event, query, m, guild);
|
m.getGuildId(),
|
||||||
}
|
TrackQuery.build(null, query),
|
||||||
}
|
new SlashPlayResponder(event, m, searchQuery));
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package creator.loki.feature.music.slash;
|
package creator.loki.feature.music.slash;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
import creator.loki.feature.music.util.GuildAudioActions;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
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 net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -20,17 +19,17 @@ public class QueueSlashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleStop(final SlashCommandInteractionEvent ev) {
|
public void handleStop(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pm.getMusicManager(ev.getGuild().getIdLong()).stop();
|
GuildAudioActions.stopAndDisconnect(
|
||||||
ev.getGuild().getJDA().getDirectAudioController().disconnect(ev.getGuild());
|
pm.getMusicManager(ev.getGuild().getIdLong()), ev.getGuild());
|
||||||
ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена."))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleSkip(final SlashCommandInteractionEvent ev) {
|
public void handleSkip(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||||
|
|
@ -45,77 +44,44 @@ public class QueueSlashHandler {
|
||||||
|
|
||||||
public void handleQueue(final SlashCommandInteractionEvent ev) {
|
public void handleQueue(final SlashCommandInteractionEvent ev) {
|
||||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||||
var cur = s.getCurrentTrack();
|
var embed = QueueView.queue(s);
|
||||||
var q = s.getQueue();
|
if (embed == null) {
|
||||||
if (cur == null && q.isEmpty()) {
|
|
||||||
ev.reply("Очередь пуста.").queue();
|
ev.reply("Очередь пуста.").queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242));
|
ev.replyEmbeds(embed).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleNowPlaying(final SlashCommandInteractionEvent ev) {
|
public void handleNowPlaying(final SlashCommandInteractionEvent ev) {
|
||||||
var track = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().getCurrentTrack();
|
var guildId = ev.getGuild().getIdLong();
|
||||||
if (track == null) {
|
var link = pm.getLavalinkManager().getClient().getLinkIfCached(guildId);
|
||||||
|
var embed = QueueView.nowPlaying(pm.getMusicManager(guildId).getScheduler(), link);
|
||||||
|
if (embed == null) {
|
||||||
ev.reply("Сейчас ничего не играет.").queue();
|
ev.reply("Сейчас ничего не играет.").queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
ev.replyEmbeds(embed).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleVolume(final SlashCommandInteractionEvent ev) {
|
public void handleVolume(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong());
|
||||||
var opt = ev.getOption("percent");
|
var opt = ev.getOption("percent");
|
||||||
if (opt == null) {
|
if (opt == null) {
|
||||||
var cur =
|
ev.replyEmbeds(
|
||||||
link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100;
|
EmbedTemplates.info("Громкость", "Текущая: **" + VolumeControl.current(link) + "%**"))
|
||||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + cur + "%**")).queue();
|
.queue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var vol = Math.max(0, Math.min(100, opt.getAsInt()));
|
var vol = VolumeControl.clamp(opt.getAsInt());
|
||||||
if (link != null && link.getCachedPlayer() != null) {
|
VolumeControl.set(link, vol, LOG);
|
||||||
link.createOrUpdatePlayer().setVolume(vol).subscribe(null, LOG);
|
|
||||||
}
|
|
||||||
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
|
ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handlePause(final SlashCommandInteractionEvent ev) {
|
public void handlePause(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||||
|
|
@ -125,7 +91,7 @@ public class QueueSlashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleResume(final SlashCommandInteractionEvent ev) {
|
public void handleResume(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pm.getMusicManager(ev.getGuild().getIdLong())
|
pm.getMusicManager(ev.getGuild().getIdLong())
|
||||||
|
|
@ -135,7 +101,7 @@ public class QueueSlashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleShuffle(final SlashCommandInteractionEvent ev) {
|
public void handleShuffle(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle();
|
pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle();
|
||||||
|
|
@ -143,7 +109,7 @@ public class QueueSlashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleRepeat(final SlashCommandInteractionEvent ev) {
|
public void handleRepeat(final SlashCommandInteractionEvent ev) {
|
||||||
if (reqVoice(ev) == null) {
|
if (VoiceCheck.requireChannel(ev) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler();
|
||||||
|
|
@ -153,29 +119,6 @@ public class QueueSlashHandler {
|
||||||
ev.replyEmbeds(EmbedTemplates.info("Повтор", d)).queue();
|
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 =
|
private static final Consumer<Throwable> LOG =
|
||||||
err ->
|
err ->
|
||||||
LoggerFactory.getLogger(QueueSlashHandler.class).warn("Lavalink: {}", err.getMessage());
|
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.H;
|
||||||
import static creator.loki.feature.profile.render.RankCardConstants.OVERLAY;
|
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.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_GAP;
|
||||||
import static creator.loki.feature.profile.render.RankCardConstants.STAT_H;
|
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.W;
|
||||||
import static creator.loki.feature.profile.render.RankCardConstants.WHITE;
|
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.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
import creator.loki.feature.radio.service.RadioManager;
|
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 {
|
public class RadioCommand implements Command {
|
||||||
private final RadioManager radioManager;
|
private final RadioSubcommands subcommands;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RadioCommand(RadioManager radioManager) {
|
public RadioCommand(RadioManager radioManager) {
|
||||||
this.radioManager = radioManager;
|
this.subcommands = new RadioSubcommands(radioManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -41,146 +39,29 @@ public class RadioCommand implements Command {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(final CommandContext context) {
|
public void execute(final CommandContext context) {
|
||||||
Guild guild = context.getGuild();
|
if (context.getGuild() == null) {
|
||||||
if (guild == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> args = context.getArgs();
|
List<String> args = context.getArgs();
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
context
|
sendUsage(context);
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Использование: `" + getUsage() + "`"))
|
|
||||||
.queue();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String sub = args.get(0).toLowerCase();
|
switch (args.get(0).toLowerCase()) {
|
||||||
switch (sub) {
|
case "set" -> subcommands.set(context, args);
|
||||||
case "set":
|
case "on" -> subcommands.on(context);
|
||||||
handleSet(context, args);
|
case "off" -> subcommands.off(context);
|
||||||
break;
|
case "volume" -> subcommands.volume(context, args);
|
||||||
case "on":
|
default -> sendUsage(context);
|
||||||
handleOn(context);
|
|
||||||
break;
|
|
||||||
case "off":
|
|
||||||
handleOff(context);
|
|
||||||
break;
|
|
||||||
case "volume":
|
|
||||||
handleVolume(context, args);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context
|
|
||||||
.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedTemplates.error(
|
|
||||||
"Радио", "Неизвестная подкоманда. Использование: `" + getUsage() + "`"))
|
|
||||||
.queue();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSet(final CommandContext context, final List<String> args) {
|
private void sendUsage(final CommandContext context) {
|
||||||
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
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(EmbedTemplates.success("Радио", "URL радио потока установлен."))
|
.sendMessageEmbeds(EmbedTemplates.error("Радио", "Использование: `" + getUsage() + "`"))
|
||||||
.queue();
|
.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 java.util.List;
|
||||||
|
|
||||||
import creator.loki.feature.radio.service.RadioManager;
|
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.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.events.message.MessageReceivedEvent;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class RadioBotListener extends ListenerAdapter {
|
public class RadioBotListener extends ListenerAdapter {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RadioBotListener.class);
|
|
||||||
private static final String PREFIX = "r!";
|
private static final String PREFIX = "r!";
|
||||||
|
|
||||||
private final RadioManager radioManager;
|
private final RadioBotCommands commands;
|
||||||
|
|
||||||
public RadioBotListener(RadioManager radioManager) {
|
public RadioBotListener(RadioManager radioManager) {
|
||||||
this.radioManager = radioManager;
|
this.commands = new RadioBotCommands(radioManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -50,115 +44,14 @@ public class RadioBotListener extends ListenerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case "play":
|
case "play" -> commands.play(event, args);
|
||||||
handlePlay(event, args);
|
case "stop" -> commands.stop(event);
|
||||||
break;
|
case "volume" -> commands.volume(event, args);
|
||||||
case "stop":
|
case "np", "nowplaying" -> commands.nowPlaying(event);
|
||||||
handleStop(event);
|
case "help", "commands" -> commands.help(event);
|
||||||
break;
|
default -> {
|
||||||
case "volume":
|
/* ignore */
|
||||||
handleVolume(event, args);
|
}
|
||||||
break;
|
|
||||||
case "np":
|
|
||||||
case "nowplaying":
|
|
||||||
handleNowPlaying(event);
|
|
||||||
break;
|
|
||||||
case "help":
|
|
||||||
case "commands":
|
|
||||||
handleHelp(event);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
|
||||||
import creator.loki.feature.music.repository.QueueRepository;
|
import creator.loki.feature.music.repository.QueueRepository;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import dev.arbjerg.lavalink.client.Link;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -19,10 +16,11 @@ import org.slf4j.LoggerFactory;
|
||||||
public class RadioManager {
|
public class RadioManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RadioManager.class);
|
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 final QueueRepository queueRepository;
|
||||||
private LavalinkManager lavalinkManager;
|
private LavalinkManager lavalinkManager;
|
||||||
private JDA radioJda;
|
private JDA radioJda;
|
||||||
|
private RadioPlayer player;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RadioManager(QueueRepository queueRepository) {
|
public RadioManager(QueueRepository queueRepository) {
|
||||||
|
|
@ -31,18 +29,26 @@ public class RadioManager {
|
||||||
|
|
||||||
public void setLavalinkManager(final LavalinkManager lm) {
|
public void setLavalinkManager(final LavalinkManager lm) {
|
||||||
this.lavalinkManager = lm;
|
this.lavalinkManager = lm;
|
||||||
|
rebuildPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRadioJda(JDA radioJda) {
|
public void setRadioJda(JDA radioJda) {
|
||||||
this.radioJda = 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) {
|
public void setRadio(final long guildId, String streamUrl) {
|
||||||
radioStates.compute(
|
radioSessions.compute(
|
||||||
guildId,
|
guildId,
|
||||||
(id, state) -> {
|
(id, state) -> {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
state = new GuildRadioState();
|
state = new RadioSession();
|
||||||
}
|
}
|
||||||
state.streamUrl = streamUrl;
|
state.streamUrl = streamUrl;
|
||||||
return state;
|
return state;
|
||||||
|
|
@ -51,16 +57,16 @@ public class RadioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRadioUrl(final long guildId) {
|
public String getRadioUrl(final long guildId) {
|
||||||
GuildRadioState state = radioStates.get(guildId);
|
RadioSession state = radioSessions.get(guildId);
|
||||||
return state != null ? state.streamUrl : null;
|
return state != null ? state.streamUrl : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startRadio(final long guildId, final long channelId) {
|
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()) {
|
if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (radioJda == null || lavalinkManager == null) {
|
if (player == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,44 +76,14 @@ public class RadioManager {
|
||||||
stopRadio(guildId);
|
stopRadio(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuildRadioState finalState = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState());
|
RadioSession finalState = radioSessions.computeIfAbsent(guildId, id -> new RadioSession());
|
||||||
finalState.streamUrl = url;
|
finalState.streamUrl = url;
|
||||||
finalState.volume = vol;
|
finalState.volume = vol;
|
||||||
|
finalState.musicManager = player.connect(guildId, channelId, vol, url);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopRadio(final long guildId) {
|
public void stopRadio(final long guildId) {
|
||||||
GuildRadioState state = radioStates.remove(guildId);
|
RadioSession state = radioSessions.remove(guildId);
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -115,23 +91,14 @@ public class RadioManager {
|
||||||
state.musicManager.stop();
|
state.musicManager.stop();
|
||||||
state.musicManager.destroy();
|
state.musicManager.destroy();
|
||||||
}
|
}
|
||||||
if (lavalinkManager != null) {
|
if (player != null) {
|
||||||
Link link = lavalinkManager.getClient().getLinkIfCached(guildId);
|
player.disconnect(guildId);
|
||||||
if (link != null) {
|
|
||||||
link.destroy().subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (radioJda != null) {
|
|
||||||
Guild guild = radioJda.getGuildById(guildId);
|
|
||||||
if (guild != null) {
|
|
||||||
guild.getAudioManager().closeAudioConnection();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
LoggerDecorator.music(logger, "Radio stopped for guild " + guildId);
|
LoggerDecorator.music(logger, "Radio stopped for guild " + guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRadioActive(final long guildId) {
|
public boolean isRadioActive(final long guildId) {
|
||||||
GuildRadioState state = radioStates.get(guildId);
|
RadioSession state = radioSessions.get(guildId);
|
||||||
return state != null && state.streamUrl != null;
|
return state != null && state.streamUrl != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,12 +111,12 @@ public class RadioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getVolume(final long guildId) {
|
public int getVolume(final long guildId) {
|
||||||
GuildRadioState state = radioStates.get(guildId);
|
RadioSession state = radioSessions.get(guildId);
|
||||||
return state != null ? state.volume : 100;
|
return state != null ? state.volume : 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVolume(final long guildId, int volume) {
|
public void setVolume(final long guildId, int volume) {
|
||||||
GuildRadioState state = radioStates.get(guildId);
|
RadioSession state = radioSessions.get(guildId);
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return;
|
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