Add Discord bot support for sending appeals to specified channels and integrate with appeal flow

This commit is contained in:
2025-08-23 23:51:45 +02:00
parent 0b4c1ccebf
commit ffddffa8dc
5 changed files with 224 additions and 2 deletions
@@ -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)'"));
}
}