first commit
This commit is contained in:
commit
32b1165bb3
32 changed files with 1763 additions and 0 deletions
21
discord/build.gradle.kts
Normal file
21
discord/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("com.gradleup.shadow")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("net.dv8tion:JDA:6.4.0")
|
||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||
runtimeOnly("ch.qos.logback:logback-classic:1.5.31")
|
||||
}
|
||||
|
||||
tasks.named<ShadowJar>("shadowJar") {
|
||||
archiveBaseName.set("discord")
|
||||
archiveClassifier.set("standalone")
|
||||
|
||||
manifest {
|
||||
attributes["Main-Class"] = "dev.loki.loAuth.discord.BotLauncher"
|
||||
}
|
||||
}
|
||||
48
discord/src/main/java/dev/loki/loAuth/discord/Bot.java
Normal file
48
discord/src/main/java/dev/loki/loAuth/discord/Bot.java
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package dev.loki.loAuth.discord;
|
||||
|
||||
import dev.loki.loAuth.common.db.DatabaseManager;
|
||||
import dev.loki.loAuth.discord.auth.AuthService;
|
||||
import dev.loki.loAuth.discord.cfg.BotConfig;
|
||||
import dev.loki.loAuth.discord.listener.LoginReactionListener;
|
||||
import dev.loki.loAuth.discord.listener.VerifyListener;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class Bot {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Bot.class);
|
||||
|
||||
private final BotConfig cfg;
|
||||
private final DatabaseManager db;
|
||||
private final JDA jda;
|
||||
private final AuthService auth;
|
||||
|
||||
public Bot(final BotConfig cfg) {
|
||||
this.cfg = cfg;
|
||||
this.db = new DatabaseManager(cfg, "loAuthBot", 10);
|
||||
this.jda = JDABuilder
|
||||
.createDefault(cfg.getToken(),
|
||||
GatewayIntent.DIRECT_MESSAGES,
|
||||
GatewayIntent.DIRECT_MESSAGE_REACTIONS,
|
||||
GatewayIntent.MESSAGE_CONTENT)
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.build();
|
||||
this.auth = new AuthService(db, cfg, jda);
|
||||
|
||||
jda.addEventListener(
|
||||
new VerifyListener(auth),
|
||||
new LoginReactionListener(db)
|
||||
);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
auth.close();
|
||||
db.close();
|
||||
jda.shutdown();
|
||||
log.info("loAuth bot shut down.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.loAuth.discord;
|
||||
|
||||
import dev.loki.loAuth.discord.cfg.BotCfg;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class BotLauncher {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BotLauncher.class);
|
||||
|
||||
public static void main(final String[] args) {
|
||||
BotCfg cfg;
|
||||
try {
|
||||
cfg = new BotCfg(getOrCreateConfig("config.yml"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load config: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
Bot bot = new Bot(cfg);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(bot::shutdown, "shutdown"));
|
||||
|
||||
log.info("loAuth bot started.");
|
||||
}
|
||||
|
||||
private static Path getOrCreateConfig(final String fileName) throws IOException {
|
||||
Path path = Path.of(System.getProperty("user.dir")).resolve(fileName);
|
||||
if (Files.notExists(path)) {
|
||||
try (InputStream in = BotLauncher.class.getResourceAsStream("/" + fileName)) {
|
||||
if (in == null) throw new IOException("Resource not found: " + fileName);
|
||||
Files.copy(in, path);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package dev.loki.loAuth.discord.auth;
|
||||
|
||||
import dev.loki.loAuth.common.db.DatabaseManager;
|
||||
import dev.loki.loAuth.discord.cfg.BotConfig;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public final class AuthService implements AutoCloseable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuthService.class);
|
||||
|
||||
private final DatabaseManager db;
|
||||
private final BotConfig cfg;
|
||||
private final JDA jda;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
public AuthService(final DatabaseManager db, final BotConfig cfg, final JDA jda) {
|
||||
this.db = db;
|
||||
this.cfg = cfg;
|
||||
this.jda = jda;
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "auth-expiry");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
int ttl = cfg.getAuthTimeoutSeconds();
|
||||
scheduler.scheduleAtFixedRate(this::kickExpiredLogins, ttl, ttl, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void verifyNewUser(final String code, final String discordId) {
|
||||
Optional<String> nick = db.consumeVerificationCode(code.toUpperCase());
|
||||
if (nick.isEmpty()) throw new IllegalArgumentException("Invalid or expired code: " + code);
|
||||
db.addUser(nick.get(), discordId);
|
||||
log.info("New user registered: {} -> {}", nick.get(), discordId);
|
||||
}
|
||||
|
||||
public void requestLoginApproval(final String minecraftNick, final String discordId) {
|
||||
String token = db.createLoginToken(minecraftNick, discordId);
|
||||
|
||||
User discordUser = jda.retrieveUserById(discordId).complete();
|
||||
discordUser.openPrivateChannel().queue(channel ->
|
||||
channel.sendMessage(
|
||||
"🔐 **Запрос на вход**\n" +
|
||||
"Игрок **" + minecraftNick + "** пытается зайти на сервер.\n" +
|
||||
"Поставь реакцию ✅ на это сообщение чтобы разрешить вход.\n" +
|
||||
"Сообщение истекает через " + cfg.getAuthTimeoutSeconds() + " секунд."
|
||||
).queue(message -> {
|
||||
message.addReaction(net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode("✅")).queue();
|
||||
db.updateLoginMessageId(token, message.getId());
|
||||
}),
|
||||
error -> log.warn("Cannot send DM to {}: {}", discordId, error.getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
public void approveLogin(final String discordId) {
|
||||
db.findByDiscordId(discordId).ifPresent(user -> {
|
||||
Optional<String> nick = db.consumeLoginToken(findTokenByDiscordId(discordId));
|
||||
nick.ifPresent(n -> log.info("Login approved for {} ({})", n, discordId));
|
||||
});
|
||||
}
|
||||
|
||||
private String findTokenByDiscordId(final String discordId) {
|
||||
// Ищем токен через БД — запрос по discord_id в pending_logins
|
||||
// consumeLoginToken вызывается из LoginReactionListener напрямую по token из message
|
||||
return discordId; // placeholder — см. LoginReactionListener
|
||||
}
|
||||
|
||||
private void kickExpiredLogins() {
|
||||
try {
|
||||
List<String> expired = db.popExpiredLogins();
|
||||
if (!expired.isEmpty())
|
||||
log.info("Expired logins cleaned up: {}", expired);
|
||||
} catch (Exception e) {
|
||||
log.error("Error during expired login cleanup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package dev.loki.loAuth.discord.cfg;
|
||||
|
||||
import dev.loki.loAuth.common.cfg.YamlConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class BotCfg implements BotConfig {
|
||||
|
||||
private final YamlConfig cfg;
|
||||
|
||||
public BotCfg(final Path configPath) throws IOException {
|
||||
cfg = new YamlConfig(configPath);
|
||||
}
|
||||
|
||||
@Override public String getToken() { return cfg.getRequired("token", String.class); }
|
||||
@Override public String getDbType() { return cfg.getRequired("db_type", String.class); }
|
||||
@Override public String getDbHost() { return cfg.getRequired("db_host", String.class); }
|
||||
@Override public int getDbPort() { return cfg.getRequired("db_port", Integer.class); }
|
||||
@Override public String getDbName() { return cfg.getRequired("db_name", String.class); }
|
||||
@Override public String getDbUser() { return cfg.getRequired("db_user", String.class); }
|
||||
@Override public String getDbPassword() { return cfg.getRequired("db_password", String.class); }
|
||||
@Override public int getAuthTimeoutSeconds() { return cfg.getRequired("auth_timeout_seconds", Integer.class); }
|
||||
@Override public int getVerificationCodeLength() { return cfg.getRequired("verification_code_length",Integer.class); }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package dev.loki.loAuth.discord.cfg;
|
||||
|
||||
import dev.loki.loAuth.common.cfg.DbConfig;
|
||||
|
||||
public interface BotConfig extends DbConfig {
|
||||
String getToken();
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package dev.loki.loAuth.discord.listener;
|
||||
|
||||
import dev.loki.loAuth.common.db.DatabaseManager;
|
||||
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class LoginReactionListener extends ListenerAdapter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LoginReactionListener.class);
|
||||
private static final String CHECKMARK = "✅";
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public LoginReactionListener(final DatabaseManager db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReactionAdd(final MessageReactionAddEvent event) {
|
||||
if (event.getUser() == null || event.getUser().isBot()) return;
|
||||
if (!CHECKMARK.equals(event.getReaction().getEmoji().getName())) return;
|
||||
|
||||
// Ищем токен по message_id — бот сохранил его при отправке
|
||||
db.consumeLoginTokenByMessageId(event.getMessageId()).ifPresent(nick ->
|
||||
log.info("Login approved for {} via reaction.", nick)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package dev.loki.loAuth.discord.listener;
|
||||
|
||||
import dev.loki.loAuth.common.exception.DuplicateUserException;
|
||||
import dev.loki.loAuth.discord.auth.AuthService;
|
||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
|
||||
public final class VerifyListener extends ListenerAdapter {
|
||||
|
||||
private static final String COMMAND = "!verify";
|
||||
|
||||
private final AuthService auth;
|
||||
|
||||
public VerifyListener(final AuthService auth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(final MessageReceivedEvent event) {
|
||||
if (event.getAuthor().isBot()) return;
|
||||
if (!event.isFromType(ChannelType.PRIVATE)) return;
|
||||
|
||||
String content = event.getMessage().getContentRaw().trim();
|
||||
if (!content.toUpperCase().startsWith(COMMAND)) return;
|
||||
|
||||
String[] parts = content.split("\\s+", 2);
|
||||
if (parts.length < 2) {
|
||||
event.getChannel().sendMessage("Использование: `!verify КОД`").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String code = parts[1].trim().toUpperCase();
|
||||
String discordId = event.getAuthor().getId();
|
||||
|
||||
try {
|
||||
auth.verifyNewUser(code, discordId);
|
||||
event.getChannel().sendMessage("✅ Ты успешно зарегистрирован! Можешь заходить на сервер.").queue();
|
||||
} catch (IllegalArgumentException e) {
|
||||
event.getChannel().sendMessage("❌ Неверный или просроченный код. Зайди на сервер снова чтобы получить новый.").queue();
|
||||
} catch (DuplicateUserException e) {
|
||||
event.getChannel().sendMessage("⚠️ Этот Discord аккаунт уже привязан к другому игроку.").queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
discord/src/main/resources/config.yml
Normal file
12
discord/src/main/resources/config.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
mode: "standalone" # standalone | embedded
|
||||
token: "token_here"
|
||||
|
||||
db_type: "postgresql" # postgresql | sqlite
|
||||
db_host: "localhost"
|
||||
db_port: 5432
|
||||
db_name: "mydb"
|
||||
db_user: "postgres"
|
||||
db_password: "secret"
|
||||
|
||||
auth_timeout_seconds: 120
|
||||
verification_code_length: 6
|
||||
Loading…
Add table
Add a link
Reference in a new issue