first commit

This commit is contained in:
loki5512344 2026-06-10 22:59:27 +02:00
commit 32b1165bb3
32 changed files with 1763 additions and 0 deletions

21
discord/build.gradle.kts Normal file
View 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"
}
}

View 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.");
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
package dev.loki.loAuth.discord.cfg;
import dev.loki.loAuth.common.cfg.DbConfig;
public interface BotConfig extends DbConfig {
String getToken();
}

View file

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

View file

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

View 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