Add Discord bot support for sending appeals to specified channels and integrate with appeal flow
This commit is contained in:
parent
0b4c1ccebf
commit
ffddffa8dc
|
|
@ -37,6 +37,7 @@ public class AppealController implements AppealsApi {
|
|||
|
||||
private final AppealDataMapper mapper;
|
||||
private final AppealMail appealMail;
|
||||
private final com.alttd.altitudeweb.services.discord.AppealDiscord appealDiscord;
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
|
|
@ -66,6 +67,12 @@ public class AppealController implements AppealsApi {
|
|||
if (history == null) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(404), "History not found");
|
||||
}
|
||||
// Send to Discord channels
|
||||
try {
|
||||
appealDiscord.sendAppealToDiscord(appeal, history);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send appeal {} to Discord", appeal.id(), e);
|
||||
}
|
||||
|
||||
CompletableFuture<Optional<EmailVerification>> emailVerificationCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
package com.alttd.altitudeweb.services.discord;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannel;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryCountMapper;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||
import com.alttd.altitudeweb.database.litebans.UserType;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.webinterface.bot.DiscordSender;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AppealDiscord {
|
||||
|
||||
private static final String OUTPUT_TYPE = "EVIDENCE";
|
||||
|
||||
public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) {
|
||||
// Fetch channels
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||
try {
|
||||
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||
channelsFuture.complete(channels);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||
channelsFuture.complete(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch counts for user
|
||||
CompletableFuture<Integer> bansF = getCountAsync(HistoryType.BAN, appeal.uuid());
|
||||
CompletableFuture<Integer> mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid());
|
||||
CompletableFuture<Integer> warnsF = getCountAsync(HistoryType.WARN, appeal.uuid());
|
||||
CompletableFuture<Integer> kicksF = getCountAsync(HistoryType.KICK, appeal.uuid());
|
||||
|
||||
List<OutputChannel> channels = channelsFuture.join();
|
||||
int bans = bansF.join();
|
||||
int mutes = mutesF.join();
|
||||
int warns = warnsF.join();
|
||||
int kicks = kicksF.join();
|
||||
|
||||
if (channels.isEmpty()) {
|
||||
log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
String message = buildMessage(appeal, history, bans, mutes, warns, kicks);
|
||||
|
||||
List<Long> channelIds = channels.stream()
|
||||
.map(OutputChannel::channel)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
DiscordSender.getInstance().sendMessageToChannels(channelIds, message);
|
||||
}
|
||||
|
||||
private CompletableFuture<Integer> getCountAsync(HistoryType type, java.util.UUID uuid) {
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> {
|
||||
try {
|
||||
Integer count = sql.getMapper(HistoryCountMapper.class)
|
||||
.getUuidPunishmentCount(type, UserType.PLAYER, uuid);
|
||||
future.complete(count == null ? 0 : count);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load punishment count for {} ({})", type, uuid, e);
|
||||
future.complete(0);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
private String buildMessage(Appeal appeal, HistoryRecord history, int bans, int mutes, int warns, int kicks) {
|
||||
boolean active = history.getUntil() == null || history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis();
|
||||
String createdAt = formatInstant(appeal.createdAt());
|
||||
return new StringBuilder()
|
||||
.append("New Appeal Submitted\n")
|
||||
.append("Username: ").append(appeal.username()).append("\n")
|
||||
.append("UUID: ").append(appeal.uuid()).append("\n")
|
||||
.append("Email: ").append(appeal.email()).append("\n")
|
||||
.append("Submitted at: ").append(createdAt).append("\n")
|
||||
.append("Punishment type: ").append(appeal.historyType()).append("\n")
|
||||
.append("Punishment id: ").append(appeal.historyId()).append("\n")
|
||||
.append("Reason: ").append(history.getReason()).append("\n")
|
||||
.append("Active: ").append(active).append("\n\n")
|
||||
.append("Appeal:\n").append(appeal.reason()).append("\n\n")
|
||||
.append("Previous punishments:\n")
|
||||
.append("- Bans: ").append(bans).append("\n")
|
||||
.append("- Mutes: ").append(mutes).append("\n")
|
||||
.append("- Warnings: ").append(warns).append("\n")
|
||||
.append("- Kicks: ").append(kicks)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private String formatInstant(Instant instant) {
|
||||
if (instant == null) return "unknown";
|
||||
return instant.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,34 @@
|
|||
package com.alttd.webinterface.bot;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
|
||||
@Slf4j
|
||||
public class DiscordBotInstance {
|
||||
|
||||
@Getter
|
||||
private JDA jda;
|
||||
private volatile boolean ready = false;
|
||||
|
||||
public void start(String token) {
|
||||
public synchronized void start(String token) {
|
||||
if (jda != null) {
|
||||
return;
|
||||
}
|
||||
jda = JDABuilder.createDefault(token,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.GUILD_PRESENCES,
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT)
|
||||
.build();
|
||||
.addEventListeners(new ReadyListener(() -> {
|
||||
ready = true;
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready && jda != null && jda.getStatus() == JDA.Status.CONNECTED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package com.alttd.webinterface.bot;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class DiscordSender {
|
||||
|
||||
private static final DiscordSender INSTANCE = new DiscordSender();
|
||||
|
||||
private final DiscordBotInstance botInstance = new DiscordBotInstance();
|
||||
|
||||
private DiscordSender() {}
|
||||
|
||||
public static DiscordSender getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private void ensureStarted() {
|
||||
if (botInstance.getJda() != null) return;
|
||||
String token = Optional.ofNullable(System.getenv("DISCORD_TOKEN"))
|
||||
.orElse(System.getProperty("DISCORD_TOKEN"));
|
||||
if (token == null || token.isBlank()) {
|
||||
log.error("Discord token not found. Set DISCORD_TOKEN as an environment variable or system property.");
|
||||
return;
|
||||
}
|
||||
botInstance.start(token);
|
||||
}
|
||||
|
||||
public void sendMessageToChannels(List<Long> channelIds, String message) {
|
||||
ensureStarted();
|
||||
if (botInstance.getJda() == null) {
|
||||
log.error("JDA not initialized; cannot send Discord message.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!botInstance.isReady()) {
|
||||
botInstance.getJda().awaitReady();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error while waiting for JDA ready state", e);
|
||||
}
|
||||
|
||||
channelIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(id -> {
|
||||
TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id);
|
||||
if (channel == null) {
|
||||
log.warn("TextChannel with id {} not found", id);
|
||||
return;
|
||||
}
|
||||
channel.sendMessage(message).queue(
|
||||
success -> log.debug("Sent message to channel {}", id),
|
||||
error -> log.error("Failed sending message to channel {}", id, error)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.alttd.webinterface.bot;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ReadyListener extends ListenerAdapter {
|
||||
|
||||
private final Runnable onReadyCallback;
|
||||
|
||||
@Override
|
||||
public void onReady(@NotNull ReadyEvent event) {
|
||||
log.info("JDA is ready. Guilds loaded: {}", event.getJDA().getGuilds().size());
|
||||
if (onReadyCallback != null) {
|
||||
try {
|
||||
onReadyCallback.run();
|
||||
} catch (Exception e) {
|
||||
log.error("Error running onReady callback", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user