1package dev.szymonwish.core.repository;
2
3import com.zaxxer.hikari.HikariDataSource;
4import dev.szymonwish.core.model.PlayerProfile;
5import org.slf4j.Logger;
6import org.slf4j.LoggerFactory;
7
8import java.sql.*;
9import java.util.Optional;
10import java.util.UUID;
11import java.util.concurrent.CompletableFuture;
12import java.util.concurrent.ConcurrentHashMap;
13
14public final class PlayerDataRepository {
15 private static final Logger log = LoggerFactory.getLogger(PlayerDataRepository.class);
16 private final HikariDataSource dataSource;
17 private final ConcurrentHashMap<UUID, PlayerProfile> cache = new ConcurrentHashMap<>();
18
19 public PlayerDataRepository(HikariDataSource dataSource) {
20 this.dataSource = dataSource;
21 }
22
23 /** Asynchroniczne pobieranie profilu z L1 Cache i fallbackiem do bazy */
24 public CompletableFuture<Optional<PlayerProfile>> findProfileAsync(UUID uuid) {
25 PlayerProfile cached = cache.get(uuid);
26 if (cached != null) {
27 return CompletableFuture.completedFuture(Optional.of(cached));
28 }
29 return CompletableFuture.supplyAsync(() -> loadFromDatabase(uuid));
30 }
31
32 private Optional<PlayerProfile> loadFromDatabase(UUID uuid) {
33 final String query = "SELECT username, coins, rank, last_seen FROM player_profiles WHERE uuid = ? LIMIT 1";
34 try (Connection conn = dataSource.getConnection();
35 PreparedStatement stmt = conn.prepareStatement(query)) {
36 stmt.setString(1, uuid.toString());
37 try (ResultSet rs = stmt.executeQuery()) {
38 if (rs.next()) {
39 PlayerProfile profile = new PlayerProfile(
40 uuid,
41 rs.getString("username"),
42 rs.getLong("coins"),
43 rs.getString("rank"),
44 rs.getTimestamp("last_seen").toInstant()
45 );
46 cache.put(uuid, profile);
47 return Optional.of(profile);
48 }
49 }
50 } catch (SQLException ex) {
51 log.error("Nieudane zapytanie SQL dla UUID {}: {}", uuid, ex.getMessage(), ex);
52 }
53 return Optional.empty();
54 }
55}
1import { Client, GatewayIntentBits, Interaction, ChatInputCommandInteraction } from 'discord.js';
2import { Logger } from '../utils/logger';
3import { CommandRegistry } from '../commands/CommandRegistry';
4import { RateLimiter } from '../security/RateLimiter';
5
6export class DiscordClusterManager {
7 private readonly client: Client;
8 private readonly limiter = new RateLimiter({ maxRequests: 5, windowMs: 10_000 });
9
10 constructor(private readonly registry: CommandRegistry, private readonly logger: Logger) {
11 this.client = new Client({
12 intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
13 failIfNotExists: false,
14 rest: { timeout: 15_000, retries: 3 }
15 });
16 this.bindLifecycleEvents();
17 }
18
19 private bindLifecycleEvents(): void {
20 this.client.on('interactionCreate', async (interaction: Interaction) => {
21 if (!interaction.isChatInputCommand()) return;
22 await this.handleCommandDispatch(interaction);
23 });
24
25 this.client.on('shardError', (error, shardId) => {
26 this.logger.error(`[Shard ${shardId}] Błąd połączenia z websocketem:`, error);
27 });
28 }
29
30 private async handleCommandDispatch(interaction: ChatInputCommandInteraction): Promise<void> {
31 const { commandName, user } = interaction;
32 if (this.limiter.isRateLimited(user.id)) {
33 await interaction.reply({ content: 'Zbyt wiele zapytań. Odczekaj chwilę przed kolejną komendą.', ephemeral: true });
34 return;
35 }
36
37 const command = this.registry.get(commandName);
38 if (!command) {
39 this.logger.warn(`Otrzymano niezarejestrowaną komendę: ${commandName}`);
40 return;
41 }
42
43 try {
44 await command.execute(interaction);
45 } catch (error) {
46 this.logger.error(`Błąd podczas wykonywania /${commandName}:`, error);
47 const msg = { content: 'Wystąpił wewnętrzny błąd podczas przetwarzania komendy.', ephemeral: true };
48 interaction.replied || interaction.deferred ? await interaction.followUp(msg) : await interaction.reply(msg);
49 }
50 }
51}