From 7d5988539568cca21c1beb0a2057b77cd613054d Mon Sep 17 00:00:00 2001 From: akastijn Date: Sat, 22 Nov 2025 22:26:40 +0100 Subject: [PATCH] Implement Discord appeal functionality, including database schema, API endpoints, front-end form, and Discord message handling. --- .../altitudeweb/config/SecurityConfig.java | 1 + .../controllers/forms/AppealController.java | 15 +- .../BannedUserToBannedUserDtoMapper.java | 14 ++ ...DiscordAppealDtoToDiscordAppealMapper.java | 26 +++ .../services/discord/AppealDiscord.java | 101 +++++++-- .../services/forms/DiscordAppeal.java | 119 +++++++++++ .../altitudeweb/services/mail/AppealMail.java | 40 ++++ .../resources/templates/appeal-email.html | 3 +- .../templates/discord-appeal-email.html | 121 +++++++++++ .../database/web_db/forms/DiscordAppeal.java | 17 ++ .../web_db/forms/DiscordAppealMapper.java | 29 +++ .../altitudeweb/setup/InitializeWebDb.java | 23 ++ .../com/alttd/webinterface/DiscordBot.java | 2 +- .../webinterface/appeals/AppealSender.java | 1 - .../webinterface/appeals/BanToBannedUser.java | 14 ++ .../webinterface/appeals/BannedUser.java | 4 + .../appeals/DiscordAppealDiscord.java | 37 ++++ .../webinterface/bot/DiscordBotInstance.java | 8 + .../send_message/DiscordSender.java | 2 +- frontend/src/app/app.routes.ts | 11 + .../discord-appeal.component.html | 184 ++++++++++++++++ .../discord-appeal.component.scss | 121 +++++++++++ .../discord-appeal.component.ts | 199 ++++++++++++++++++ .../head-mod/staff-pt/staff-pt.component.html | 2 +- .../pages/header/header/header.component.html | 4 +- .../nick-generator.component.html | 4 +- frontend/src/app/services/auth.service.ts | 3 + open_api/src/main/resources/api.yml | 2 + .../resources/schemas/forms/appeal/appeal.yml | 17 +- .../schemas/forms/appeal/discordAppeal.yml | 57 +++++ 30 files changed, 1143 insertions(+), 38 deletions(-) create mode 100644 backend/src/main/java/com/alttd/altitudeweb/mappers/BannedUserToBannedUserDtoMapper.java create mode 100644 backend/src/main/java/com/alttd/altitudeweb/mappers/DiscordAppealDtoToDiscordAppealMapper.java create mode 100644 backend/src/main/java/com/alttd/altitudeweb/services/forms/DiscordAppeal.java create mode 100644 backend/src/main/resources/templates/discord-appeal-email.html create mode 100644 database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppeal.java create mode 100644 database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppealMapper.java create mode 100644 discord/src/main/java/com/alttd/webinterface/appeals/BanToBannedUser.java create mode 100644 discord/src/main/java/com/alttd/webinterface/appeals/BannedUser.java create mode 100644 discord/src/main/java/com/alttd/webinterface/appeals/DiscordAppealDiscord.java create mode 100644 frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.html create mode 100644 frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.scss create mode 100644 frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.ts create mode 100644 open_api/src/main/resources/schemas/forms/appeal/discordAppeal.yml diff --git a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java index c47a86c..d953edb 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java +++ b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java @@ -52,6 +52,7 @@ public class SecurityConfig { .requestMatchers("/api/login/getUsername").authenticated() .requestMatchers("/api/mail/**").authenticated() .requestMatchers("/api/site/vote").authenticated() + .requestMatchers("/api/appeal").authenticated() .requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java index 2a397d1..8935274 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java @@ -10,15 +10,14 @@ import com.alttd.altitudeweb.database.web_db.forms.AppealMapper; import com.alttd.altitudeweb.database.web_db.mail.EmailVerification; import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; import com.alttd.altitudeweb.mappers.AppealDataMapper; -import com.alttd.altitudeweb.model.DiscordAppealDto; -import com.alttd.altitudeweb.model.FormResponseDto; -import com.alttd.altitudeweb.model.MinecraftAppealDto; -import com.alttd.altitudeweb.model.UpdateMailDto; +import com.alttd.altitudeweb.model.*; +import com.alttd.altitudeweb.services.forms.DiscordAppeal; import com.alttd.altitudeweb.services.limits.RateLimit; import com.alttd.altitudeweb.services.mail.AppealMail; import com.alttd.altitudeweb.setup.Connection; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -36,12 +35,18 @@ public class AppealController implements AppealsApi { private final AppealDataMapper mapper; private final AppealMail appealMail; + private final DiscordAppeal discordAppeal; private final com.alttd.altitudeweb.services.discord.AppealDiscord appealDiscord; + @Override + public ResponseEntity getBannedUser(Long discordId) throws Exception { + return new ResponseEntity<>(discordAppeal.getBannedUser(discordId), HttpStatus.OK); + } + @RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal") @Override public ResponseEntity submitDiscordAppeal(DiscordAppealDto discordAppealDto) { - throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported"); + return new ResponseEntity<>(discordAppeal.submitAppeal(discordAppealDto), HttpStatus.OK); } @RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal") diff --git a/backend/src/main/java/com/alttd/altitudeweb/mappers/BannedUserToBannedUserDtoMapper.java b/backend/src/main/java/com/alttd/altitudeweb/mappers/BannedUserToBannedUserDtoMapper.java new file mode 100644 index 0000000..f4c4451 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/mappers/BannedUserToBannedUserDtoMapper.java @@ -0,0 +1,14 @@ +package com.alttd.altitudeweb.mappers; + +import com.alttd.altitudeweb.model.BannedUserDto; +import com.alttd.webinterface.appeals.BannedUser; +import org.springframework.stereotype.Service; + +@Service +public class BannedUserToBannedUserDtoMapper { + + public BannedUserDto map(BannedUser bannedUser) { + return new BannedUserDto(bannedUser.userId(), bannedUser.reason(), bannedUser.name(), bannedUser.avatarUrl()); + } + +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/mappers/DiscordAppealDtoToDiscordAppealMapper.java b/backend/src/main/java/com/alttd/altitudeweb/mappers/DiscordAppealDtoToDiscordAppealMapper.java new file mode 100644 index 0000000..195db04 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/mappers/DiscordAppealDtoToDiscordAppealMapper.java @@ -0,0 +1,26 @@ +package com.alttd.altitudeweb.mappers; + +import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal; +import com.alttd.altitudeweb.model.DiscordAppealDto; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.UUID; + +@Service +public class DiscordAppealDtoToDiscordAppealMapper { + + public DiscordAppeal map(DiscordAppealDto discordAppealDto, UUID loggedInUserUuid, String discordUsername) { + return new DiscordAppeal( + UUID.randomUUID(), + loggedInUserUuid, + discordAppealDto.getDiscordId(), + discordUsername, + discordAppealDto.getAppeal(), + Instant.now(), + null, + discordAppealDto.getEmail(), + null); + } + +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java b/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java index 937e067..7cb0fb3 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java @@ -11,6 +11,8 @@ 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.database.web_db.forms.AppealMapper; +import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal; +import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper; import com.alttd.altitudeweb.setup.Connection; import com.alttd.webinterface.appeals.AppealSender; import com.alttd.webinterface.objects.MessageForEmbed; @@ -33,19 +35,67 @@ public class AppealDiscord { private static final String OUTPUT_TYPE = "APPEAL"; + public void sendAppealToDiscord(DiscordAppeal discordAppeal) { + CompletableFuture> channelsFuture = getChannelListFuture(); + List channels = channelsFuture.join(); + if (channels.isEmpty()) { + log.warn("Discord appeal: No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE); + return; + } + + String createdAt = formatInstant(discordAppeal.createdAt()); + List fields = new ArrayList<>(); + + // Group: User + fields.add(new DiscordSender.EmbedField( + "User", + """ + Discord Username: %s + Discord id: %s + MC UUID: %s + Submitted: %s + """.formatted( + safe(discordAppeal.username()), + discordAppeal.discordId(), + safe(String.valueOf(discordAppeal.uuid())), + createdAt + ), + false + )); + + Optional optionalAssignedTo = assignAppeal(); + if (optionalAssignedTo.isPresent()) { + Long assignedTo = optionalAssignedTo.get(); + fields.add(new DiscordSender.EmbedField( + "Assigned to", + "Assigned to: <@" + assignedTo + ">", + true + )); + assignDiscordAppealTo(discordAppeal.id(), assignedTo); + } else { + fields.add(new DiscordSender.EmbedField( + "Assigned to", + "Assigned to: None (failed to assign)", + true + )); + } + + String description = safe(discordAppeal.reason()); + + List channelIds = channels.stream() + .map(OutputChannel::channel) + .toList(); + + // colorRgb = null (use default), timestamp = appeal.createdAt if available + Instant timestamp = discordAppeal.createdAt() != null ? discordAppeal.createdAt() : Instant.now(); + MessageForEmbed newAppealSubmitted = new MessageForEmbed( + "New Discord Appeal Submitted", description, fields, null, timestamp, null); + AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L)); + } + public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) { // Fetch channels - CompletableFuture> channelsFuture = new CompletableFuture<>(); - Connection.getConnection(Databases.DISCORD).runQuery(sql -> { - try { - List 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<>()); - } - }); + CompletableFuture> channelsFuture = getChannelListFuture(); CompletableFuture bansF = getCountAsync(HistoryType.BAN, appeal.uuid()); CompletableFuture mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid()); @@ -102,7 +152,7 @@ public class AppealDiscord { "Assigned to: <@" + assignedTo + ">", true )); - assignAppealTo(appeal.id(), assignedTo); + assignMinecraftAppealTo(appeal.id(), assignedTo); } else { fields.add(new DiscordSender.EmbedField( "Assigned to", @@ -124,7 +174,22 @@ public class AppealDiscord { AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L)); } - private void assignAppealTo(UUID appealId, Long assignedTo) { + private static CompletableFuture> getChannelListFuture() { + CompletableFuture> channelsFuture = new CompletableFuture<>(); + Connection.getConnection(Databases.DISCORD).runQuery(sql -> { + try { + List 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<>()); + } + }); + return channelsFuture; + } + + private void assignMinecraftAppealTo(UUID appealId, Long assignedTo) { Connection.getConnection(Databases.DEFAULT).runQuery(sql -> { try { sql.getMapper(AppealMapper.class).assignAppeal(appealId, assignedTo); @@ -134,6 +199,16 @@ public class AppealDiscord { }); } + private void assignDiscordAppealTo(UUID appealId, Long assignedTo) { + Connection.getConnection(Databases.DEFAULT).runQuery(sql -> { + try { + sql.getMapper(DiscordAppealMapper.class).assignDiscordAppeal(appealId, assignedTo); + } catch (Exception e) { + log.error("Failed to assign appeal to {}", assignedTo, e); + } + }); + } + private CompletableFuture getCountAsync(HistoryType type, java.util.UUID uuid) { CompletableFuture future = new CompletableFuture<>(); Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> { diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/forms/DiscordAppeal.java b/backend/src/main/java/com/alttd/altitudeweb/services/forms/DiscordAppeal.java new file mode 100644 index 0000000..f83be4e --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/services/forms/DiscordAppeal.java @@ -0,0 +1,119 @@ +package com.alttd.altitudeweb.services.forms; + +import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid; +import com.alttd.altitudeweb.database.Databases; +import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper; +import com.alttd.altitudeweb.database.web_db.mail.EmailVerification; +import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; +import com.alttd.altitudeweb.mappers.BannedUserToBannedUserDtoMapper; +import com.alttd.altitudeweb.mappers.DiscordAppealDtoToDiscordAppealMapper; +import com.alttd.altitudeweb.model.BannedUserResponseDto; +import com.alttd.altitudeweb.model.DiscordAppealDto; +import com.alttd.altitudeweb.model.FormResponseDto; +import com.alttd.altitudeweb.services.discord.AppealDiscord; +import com.alttd.altitudeweb.services.mail.AppealMail; +import com.alttd.altitudeweb.setup.Connection; +import com.alttd.webinterface.appeals.BannedUser; +import com.alttd.webinterface.appeals.DiscordAppealDiscord; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DiscordAppeal { + + private final BannedUserToBannedUserDtoMapper bannedUserToBannedUserDtoMapper; + private final DiscordAppealDtoToDiscordAppealMapper discordAppealDtoToDiscordAppealMapper; + private final AuthenticatedUuid authenticatedUuid; + private final AppealDiscord appealDiscord; + private final AppealMail appealMail; + + public BannedUserResponseDto getBannedUser(Long discordId) { + DiscordAppealDiscord discordAppeal = DiscordAppealDiscord.getInstance(); + Optional join = discordAppeal.getBannedUser(discordId).join(); + if (join.isEmpty()) { + return new BannedUserResponseDto(false); + } + BannedUserResponseDto bannedUserResponseDto = new BannedUserResponseDto(true); + bannedUserResponseDto.setBannedUser(bannedUserToBannedUserDtoMapper.map(join.get())); + return bannedUserResponseDto; + } + + public FormResponseDto submitAppeal(DiscordAppealDto discordAppealDto) { + DiscordAppealDiscord discordAppealDiscord = DiscordAppealDiscord.getInstance(); + Optional join = discordAppealDiscord.getBannedUser(discordAppealDto.getDiscordId()).join(); + if (join.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"); + } + BannedUser bannedUser = join.get(); + + Optional uuid = authenticatedUuid.tryGetAuthenticatedUserUuid(); + if (uuid.isEmpty()) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated"); + } + + CompletableFuture appealCompletableFuture = new CompletableFuture<>(); + + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> { + log.debug("Loading history by id"); + try { + com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal discordAppealRecord = discordAppealDtoToDiscordAppealMapper + .map(discordAppealDto, uuid.get(), bannedUser.name()); + sqlSession.getMapper(DiscordAppealMapper.class).createDiscordAppeal(discordAppealRecord); + appealCompletableFuture.complete(discordAppealRecord); + } catch (Exception e) { + log.error("Failed to load history count", e); + appealCompletableFuture.completeExceptionally(e); + } + }); + com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal discordAppeal = appealCompletableFuture.join(); + + CompletableFuture> emailVerificationCompletableFuture = new CompletableFuture<>(); + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> { + log.debug("Retrieving mail by uuid and address"); + + EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class) + .findByUserAndEmail(discordAppeal.uuid(), discordAppeal.email().toLowerCase()); + emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail)); + }); + Optional optionalEmailVerification = emailVerificationCompletableFuture.join(); + + if (optionalEmailVerification.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid mail"); + } + EmailVerification emailVerification = optionalEmailVerification.get(); + if (!emailVerification.verified()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Mail not verified"); + } + + try { + appealDiscord.sendAppealToDiscord(discordAppeal); + } catch (Exception e) { + log.error("Failed to send appeal {} to Discord", discordAppeal.id(), e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to send appeal to Discord"); + } + //TODO verify mail + appealMail.sendAppealNotification(discordAppeal); + + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> { + log.debug("Marking appeal {} as sent", discordAppeal.id()); + sqlSession.getMapper(DiscordAppealMapper.class) + .markDiscordAppealAsSent(discordAppeal.id()); + }); + return new FormResponseDto( + discordAppeal.id().toString(), + "Your appeal has been submitted. You will be notified when it has been reviewed.", + true); + } +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/mail/AppealMail.java b/backend/src/main/java/com/alttd/altitudeweb/services/mail/AppealMail.java index 483f745..c00d4a0 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/mail/AppealMail.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/mail/AppealMail.java @@ -2,6 +2,7 @@ package com.alttd.altitudeweb.services.mail; import com.alttd.altitudeweb.database.litebans.HistoryRecord; import com.alttd.altitudeweb.database.web_db.forms.Appeal; +import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; @@ -29,6 +30,20 @@ public class AppealMail { private static final String APPEAL_EMAIL = "appeal@alttd.com"; + /** + * Sends an email notification about the appeal to both the user and the appeals team. + * + * @param appeal The appeal object containing all necessary information + */ + public void sendAppealNotification(DiscordAppeal appeal) { + try { + sendEmailToAppealsTeam(appeal); + + log.info("Discord Appeal notification emails sent successfully for appeal ID: {}", appeal.id()); + } catch (Exception e) { + log.error("Failed to send discord appeal notification emails for appeal ID: {}", appeal.id(), e); + } + } /** * Sends an email notification about the appeal to both the user and the appeals team. * @@ -66,4 +81,29 @@ public class AppealMail { mailSender.send(message); } + private void sendEmailToAppealsTeam(DiscordAppeal appeal) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = getAppealMimeMessageHelper(appeal, message); + + Context context = new Context(); + context.setVariable("appeal", appeal); + context.setVariable("createdAt", appeal.createdAt() + .atZone(ZoneId.of("UTC")) + .format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"))); + String content = templateEngine.process("discord-appeal-email", context); + + helper.setText(content, true); + mailSender.send(message); + } + + private MimeMessageHelper getAppealMimeMessageHelper(DiscordAppeal appeal, MimeMessage message) throws MessagingException { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + helper.setFrom(fromEmail); + helper.setTo(APPEAL_EMAIL); + helper.setReplyTo(appeal.email()); + helper.setSubject("New Appeal Submitted - " + appeal.username()); + return helper; + } + } diff --git a/backend/src/main/resources/templates/appeal-email.html b/backend/src/main/resources/templates/appeal-email.html index 08018d1..e8c3d05 100644 --- a/backend/src/main/resources/templates/appeal-email.html +++ b/backend/src/main/resources/templates/appeal-email.html @@ -114,8 +114,7 @@

Appeal:

-

Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.

+

appeal

diff --git a/backend/src/main/resources/templates/discord-appeal-email.html b/backend/src/main/resources/templates/discord-appeal-email.html new file mode 100644 index 0000000..fe350ce --- /dev/null +++ b/backend/src/main/resources/templates/discord-appeal-email.html @@ -0,0 +1,121 @@ + + + + + Discord Appeal Notification + + + +
+ The Altitude Minecraft Server +

Appeal by Username

+
+
+
+

User information

+
    +
  • Username: username
  • +
  • UUID: uuid
  • +
  • Email: email
  • +
  • Submitted at: date
  • +
+
+
+
+
+

Appeal:

+

appeal

+
+
+
+
+ + diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppeal.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppeal.java new file mode 100644 index 0000000..9b7eed4 --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppeal.java @@ -0,0 +1,17 @@ +package com.alttd.altitudeweb.database.web_db.forms; + +import java.time.Instant; +import java.util.UUID; + +public record DiscordAppeal( + UUID id, + UUID uuid, + Long discordId, + String username, + String reason, + Instant createdAt, + Instant sendAt, + String email, + Long assignedTo +) { +} diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppealMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppealMapper.java new file mode 100644 index 0000000..e42302e --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/DiscordAppealMapper.java @@ -0,0 +1,29 @@ +package com.alttd.altitudeweb.database.web_db.forms; + +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; +import java.util.UUID; + +public interface DiscordAppealMapper { + + @Insert(""" + INSERT INTO discord_appeals (uuid, discord_id, discord_username, reason, created_at, send_at, e_mail, assigned_to) + VALUES (#{uuid}, #{discord_id}, #{discord_username}, #{reason}, #{createdAt}, #{sendAt}, #{email}, #{assignedTo}) + """) + void createDiscordAppeal(DiscordAppeal discordAppeal); + + @Update(""" + UPDATE discord_appeals SET send_at = NOW() + WHERE id = #{id} + """) + void markDiscordAppealAsSent(UUID id); + + @Update(""" + UPDATE discord_appeals SET assigned_to = #{assignedTo} + WHERE id = #{id} + """) + void assignDiscordAppeal(UUID id, Long assignedTo); +} diff --git a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java index f65623e..553ee9c 100644 --- a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java +++ b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java @@ -33,6 +33,7 @@ public class InitializeWebDb { createPrivilegedUsersTable(sqlSession); createPrivilegesTable(sqlSession); createAppealTable(sqlSession); + createdDiscordAppealTable(sqlSession); createStaffApplicationsTable(sqlSession); createUserEmailsTable(sqlSession); }); @@ -183,4 +184,26 @@ public class InitializeWebDb { } } + private static void createdDiscordAppealTable(@NotNull SqlSession sqlSession) { + String query = """ + CREATE TABLE IF NOT EXISTS discord_appeals ( + id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY, + uuid UUID NOT NULL, + discord_id BIGINT UNSIGNED NOT NULL, + discord_username VARCHAR(32) NOT NULL, + reason TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + send_at TIMESTAMP NULL, + e_mail TEXT NOT NULL, + assigned_to BIGINT UNSIGNED NULL, + FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE + ); + """; + try (Statement statement = sqlSession.getConnection().createStatement()) { + statement.execute(query); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } diff --git a/discord/src/main/java/com/alttd/webinterface/DiscordBot.java b/discord/src/main/java/com/alttd/webinterface/DiscordBot.java index ef315a7..eb136d9 100644 --- a/discord/src/main/java/com/alttd/webinterface/DiscordBot.java +++ b/discord/src/main/java/com/alttd/webinterface/DiscordBot.java @@ -12,7 +12,7 @@ public class DiscordBot { log.error("Discord token not found, put it in the DISCORD_TOKEN environment variable"); System.exit(1); } - DiscordBotInstance discordBotInstance = new DiscordBotInstance(); + DiscordBotInstance discordBotInstance = DiscordBotInstance.getInstance(); discordBotInstance.start(discordToken); } } diff --git a/discord/src/main/java/com/alttd/webinterface/appeals/AppealSender.java b/discord/src/main/java/com/alttd/webinterface/appeals/AppealSender.java index de8f641..c436d4d 100644 --- a/discord/src/main/java/com/alttd/webinterface/appeals/AppealSender.java +++ b/discord/src/main/java/com/alttd/webinterface/appeals/AppealSender.java @@ -9,7 +9,6 @@ import net.dv8tion.jda.api.entities.Message; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; @Slf4j public class AppealSender { diff --git a/discord/src/main/java/com/alttd/webinterface/appeals/BanToBannedUser.java b/discord/src/main/java/com/alttd/webinterface/appeals/BanToBannedUser.java new file mode 100644 index 0000000..567d809 --- /dev/null +++ b/discord/src/main/java/com/alttd/webinterface/appeals/BanToBannedUser.java @@ -0,0 +1,14 @@ +package com.alttd.webinterface.appeals; + +import net.dv8tion.jda.api.entities.Guild; + +public class BanToBannedUser { + + public static BannedUser map(Guild.Ban ban) { + return new BannedUser(ban.getUser().getIdLong(), + ban.getReason(), + ban.getUser().getEffectiveName(), + ban.getUser().getEffectiveAvatarUrl()); + } + +} diff --git a/discord/src/main/java/com/alttd/webinterface/appeals/BannedUser.java b/discord/src/main/java/com/alttd/webinterface/appeals/BannedUser.java new file mode 100644 index 0000000..52edb14 --- /dev/null +++ b/discord/src/main/java/com/alttd/webinterface/appeals/BannedUser.java @@ -0,0 +1,4 @@ +package com.alttd.webinterface.appeals; + +public record BannedUser(long userId, String reason, String name, String avatarUrl) { +} diff --git a/discord/src/main/java/com/alttd/webinterface/appeals/DiscordAppealDiscord.java b/discord/src/main/java/com/alttd/webinterface/appeals/DiscordAppealDiscord.java new file mode 100644 index 0000000..603337b --- /dev/null +++ b/discord/src/main/java/com/alttd/webinterface/appeals/DiscordAppealDiscord.java @@ -0,0 +1,37 @@ +package com.alttd.webinterface.appeals; + +import com.alttd.webinterface.bot.DiscordBotInstance; +import net.dv8tion.jda.api.entities.Guild; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class DiscordAppealDiscord { + private static final DiscordAppealDiscord INSTANCE = new DiscordAppealDiscord(); + + public static DiscordAppealDiscord getInstance() { + return INSTANCE; + } + + public CompletableFuture> getBannedUser(long discordId) { + Guild guildById = DiscordBotInstance.getInstance() + .getJda() + .getGuildById(141644560005595136L); + if (guildById == null) { + throw new IllegalStateException("Guild not found"); + } + CompletableFuture> completableFuture = new CompletableFuture<>(); + DiscordBotInstance.getInstance().getJda().retrieveUserById(discordId) + .queue(user -> { + guildById.retrieveBan(user).queue(ban -> { + if (ban == null) { + completableFuture.complete(Optional.empty()); + return; + } + completableFuture.complete(Optional.of(BanToBannedUser.map(ban))); + }); + + }); + return completableFuture; + } +} diff --git a/discord/src/main/java/com/alttd/webinterface/bot/DiscordBotInstance.java b/discord/src/main/java/com/alttd/webinterface/bot/DiscordBotInstance.java index bc74ecd..115eaa3 100644 --- a/discord/src/main/java/com/alttd/webinterface/bot/DiscordBotInstance.java +++ b/discord/src/main/java/com/alttd/webinterface/bot/DiscordBotInstance.java @@ -9,6 +9,14 @@ import net.dv8tion.jda.api.requests.GatewayIntent; @Slf4j public class DiscordBotInstance { + private static final DiscordBotInstance INSTANCE = new DiscordBotInstance(); + + public static DiscordBotInstance getInstance() { + return INSTANCE; + } + + private DiscordBotInstance() {} + @Getter private JDA jda; private volatile boolean ready = false; diff --git a/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java b/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java index 74ce3f2..829845b 100644 --- a/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java +++ b/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java @@ -22,7 +22,7 @@ public class DiscordSender { private static final DiscordSender INSTANCE = new DiscordSender(); - private final DiscordBotInstance botInstance = new DiscordBotInstance(); + private final DiscordBotInstance botInstance = DiscordBotInstance.getInstance(); private DiscordSender() {} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 04516b1..0b298b7 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -150,6 +150,11 @@ export const routes: Routes = [ redirectTo: 'forms/appeal', pathMatch: 'full' }, + { + path: 'discord-appeal', + redirectTo: 'forms/discord-appeal', + pathMatch: 'full' + }, { path: 'forms/appeal/:code', loadComponent: () => import('./pages/forms/appeal/appeal.component').then(m => m.AppealComponent), @@ -162,6 +167,12 @@ export const routes: Routes = [ canActivate: [AuthGuard], data: {requiredAuthorizations: ['SCOPE_user']} }, + { + path: 'forms/discord-appeal', + loadComponent: () => import('./pages/forms/discord-appeal/discord-appeal.component').then(m => m.DiscordAppealComponent), + canActivate: [AuthGuard], + data: {requiredAuthorizations: ['SCOPE_user']} + }, { path: 'forms/sent', loadComponent: () => import('./pages/forms/sent/sent.component').then(m => m.SentComponent), diff --git a/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.html b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.html new file mode 100644 index 0000000..5e55a95 --- /dev/null +++ b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.html @@ -0,0 +1,184 @@ +
+ +
+

Discord Appeal

+
+
+
+ +
+
+
+ @if (currentPageIndex === 0) { +
+ Discord +

Discord Appeal

+

We aim to respond within 48 hours.

+ +
+ } + + @if (currentPageIndex === 1) { +
+
+

You are logged in as {{ authService.username() }}. If this is the correct + account please continue

+
+

Notice: Submitting an appeal is not an instant process. + We will investigate the punishment you are appealing and respond within 48 hours.

+

Appeals that seem to have been made with + little to no effort will be automatically denied.

+
+ +
+ } + + @if (currentPageIndex === 2) { +
+
+

Please enter your discord id below.

+

You can find your discord id by going to User settings -> Advanced -> Developer Mode and turning it + on

+

With Developer Mode on in Discord click your profile in the bottom left and click Copy User Id

+

We use this to find your punishment on our Discord server.

+
+ + Discord Id + + + +
+ } + + @if (currentPageIndex === 3) { + @if (bannedUser == null) { +
+
+

We were unable to find your punishment on our Discord server.

+
+
+ } @else if (bannedUser.isBanned || bannedUser.bannedUser == null) { +
+
+

Your discord account is not banned on our Discord server.

+
+
+ } @else { +
+
+ Avatar for Discord user {{ bannedUser.bannedUser.name }} +

{{ bannedUser.bannedUser.name }}

+

Your punishment is: {{ bannedUser.bannedUser.reason }} +

+ +
+
) + } + } + @if (currentPageIndex >= 4) { +
+ @if (currentPageIndex === 4) { +
+
+

Please enter your email.

+

It does not have to be your minecraft email. You will have to verify + it

+ + Email + + @if (form.controls.email.invalid && form.controls.email.touched) { + + @if (form.controls.email.errors?.['required']) { + Email is required + } @else if (form.controls.email.errors?.['email']) { + Please enter a valid email address + } + + } + + @if (emailIsValid()) { +
+ + check + You have validated your email previously, and can continue to the next page! + +
+ } +
+ +
+ } + @if (currentPageIndex === 5) { +
+
+

Why should your ban be reduced or removed?

+

Please take your time writing this, we're more likely to accept an + appeal if effort was put into it.

+ + Reason + + @if (form.controls.appeal.invalid && form.controls.appeal.touched) { + + @if (form.controls.appeal.errors?.['required']) { + Reason is required + } @else if (form.controls.appeal.errors?.['minlength']) { + Reason must be at least 10 characters + } + + } + +
+ +
+ } +
+ } +
+ + + @if (totalPages.length > 1) { +
+ + + @for (i of totalPages; track i) { + + } + + +
+ } +
+
+
+
+
diff --git a/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.scss b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.scss new file mode 100644 index 0000000..13c4825 --- /dev/null +++ b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.scss @@ -0,0 +1,121 @@ +:host { + display: block; +} + +.appeal-container { + display: flex; + flex-direction: column; + height: 100%; +} + +main { + flex: 1; + display: flex; + flex-direction: column; +} + +.form-container { + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + flex: 1; +} + +.formPage { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + width: 100%; + height: 100%; + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.navigation-buttons { + display: flex; + gap: 16px; + margin-top: 20px; +} + +.form-navigation { + display: flex; + justify-content: center; + gap: 10px; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + + +.nav-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.3); + cursor: pointer; + transition: background-color 0.3s ease; + margin-top: auto; + margin-bottom: auto; + + &.active { + background-color: #fff; + } +} + +.nav-button { + color: #1f9bde; +} + +.pages { + margin-top: auto; + margin-bottom: auto; +} + +.description { + max-width: 75ch; + text-align: left; +} + +.valid-email { + display: flex; + align-items: center; + color: #4CAF50; + margin: 10px 0; + padding: 8px 12px; + border-radius: 4px; + background-color: rgba(76, 175, 80, 0.1); +} + +.valid-email mat-icon { + color: #4CAF50; + margin-right: 10px; +} + +.valid-email span { + color: #4CAF50; + font-weight: 500; +} + +.discord-avatar { + width: 128px; + height: 128px; + border-radius: 50%; + object-fit: cover; + display: block; + margin-left: auto; + margin-right: auto; +} diff --git a/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.ts b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.ts new file mode 100644 index 0000000..b51ec3e --- /dev/null +++ b/frontend/src/app/pages/forms/discord-appeal/discord-appeal.component.ts @@ -0,0 +1,199 @@ +import {Component, computed, effect, inject, OnInit, signal} from '@angular/core'; +import {AppealsService, BannedUserResponse, DiscordAppeal, EmailEntry, MailService} from '@api'; +import {FullSizeComponent} from '@shared-components/full-size/full-size.component'; +import {HeaderComponent} from '@header/header.component'; +import {MatButton, MatIconButton} from '@angular/material/button'; +import {NgOptimizedImage} from '@angular/common'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {AuthService} from '@services/auth.service'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatIconModule} from '@angular/material/icon'; +import {VerifyMailDialogComponent} from '@pages/forms/verify-mail-dialog/verify-mail-dialog.component'; +import {MatDialog} from '@angular/material/dialog'; +import {Router} from '@angular/router'; + +@Component({ + selector: 'app-discord-appeal', + imports: [ + FullSizeComponent, + HeaderComponent, + MatButton, + MatProgressSpinnerModule, + NgOptimizedImage, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatIconModule, + ReactiveFormsModule, + MatIconButton, + ], + templateUrl: './discord-appeal.component.html', + styleUrl: './discord-appeal.component.scss' +}) +export class DiscordAppealComponent implements OnInit { + private readonly appealService: AppealsService = inject(AppealsService) + private readonly mailService = inject(MailService); + private readonly dialog = inject(MatDialog); + private readonly router = inject(Router) + + private emails = signal([]); + + protected readonly authService = inject(AuthService); + + protected bannedUser: BannedUserResponse | null = null; + protected discordId: string = window.location.hostname === 'localhost' ? '212303885988134914' : ''; + protected verifiedEmails = computed(() => this.emails() + .filter(email => { + console.log(email.verified) + return email.verified + }) + .map(email => { + console.log(email.email.toLowerCase()) + return email.email.toLowerCase() + })); + protected emailIsValid = signal(false); + protected currentPageIndex: number = 0; + protected totalPages: number[] = [0]; + protected form: FormGroup; + + constructor() { + this.form = new FormGroup({ + email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}), + appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]}) + }); + effect(() => { + if (this.verifiedEmails().length > 0) { + console.log('verified emails') + console.log(this.verifiedEmails()[0]) + this.form.get('email')?.setValue(this.verifiedEmails()[0]); + this.emailIsValid.set(true); + } + }); + } + + ngOnInit(): void { + if (window.location.hostname === 'localhost') { + this.emails.set([{email: 'dev@alttd.com', verified: true}]) + } else { + this.mailService.getUserEmails().subscribe(emails => { + this.emails.set(emails); + }); + } + this.form.valueChanges.subscribe(() => { + if (this.verifiedEmails().includes(this.form.getRawValue().email.toLowerCase())) { + this.emailIsValid.set(true); + } else { + this.emailIsValid.set(false); + } + }); + + } + + protected validateMailOrNextPage() { + if (this.emailIsValid()) { + this.nextPage(); + return; + } + const dialogRef = this.dialog.open(VerifyMailDialogComponent, { + data: {email: this.form.getRawValue().email}, + }); + dialogRef.afterClosed().subscribe(result => { + if (result === true) { + this.emailIsValid.set(true); + } + }); + } + + public goToPage(pageIndex: number): void { + if (pageIndex >= 0 && pageIndex < this.totalPages.length) { + this.currentPageIndex = pageIndex; + } + } + + public previousPage() { + this.goToPage(this.currentPageIndex - 1); + } + + public nextPage() { + if (this.currentPageIndex === this.totalPages.length - 1) { + this.totalPages.push(this.currentPageIndex + 1); + } + this.goToPage(this.currentPageIndex + 1); + } + + public isFirstPage(): boolean { + return this.currentPageIndex === 0; + } + + public isLastPage(): boolean { + return this.currentPageIndex === this.totalPages.length - 1; + } + + protected checkPunishment() { + if (window.location.hostname === 'localhost') { + this.bannedUser = { + isBanned: false, + bannedUser: { + userId: 212303885988134914, + reason: "This is a test punishment", + name: "stijn", + avatarUrl: "https://cdn.discordapp.com/avatars/212303885988134914/3a264be54ca7208d638a22143fc8fdb8.webp?size=160" + } + } + this.nextPage(); + return + } + this.appealService.getBannedUser(Number(this.discordId)) + .subscribe(user => { + this.bannedUser = user + this.nextPage(); + }); + } + + protected onSubmit() { + if (this.form === undefined) { + console.error('Form is undefined'); + return + } + if (this.form.valid) { + this.sendForm() + } else { + Object.keys(this.form.controls).forEach(field => { + const control = this.form!.get(field); + if (!(control instanceof FormGroup)) { + console.error('Control [' + control + '] is not a FormGroup'); + return; + } + control.markAsTouched({onlySelf: true}); + }); + } + } + + private sendForm() { + const rawValue = this.form.getRawValue(); + const uuid = this.authService.getUuid(); + if (uuid === null) { + throw new Error('JWT subject is null, are you logged in?'); + } + const appeal: DiscordAppeal = { + discordId: Number(this.discordId), + appeal: rawValue.appeal, + email: rawValue.email, + } + this.appealService.submitDiscordAppeal(appeal).subscribe((result) => { + if (!result.verified_mail) { + throw new Error('Mail not verified'); + } + this.router.navigate(['/forms/sent'], { + state: {message: result.message} + }).then(); + }) + } +} + +interface WebDiscordAppeal { + email: FormControl; + appeal: FormControl; +} diff --git a/frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.html b/frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.html index aba5423..e50fee4 100644 --- a/frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.html +++ b/frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.html @@ -34,7 +34,7 @@ - @if (!staffPt()?.length) { + @if (!staffPt().length) { No data for this week. diff --git a/frontend/src/app/pages/header/header/header.component.html b/frontend/src/app/pages/header/header/header.component.html index 79f68ea..6d45cf1 100644 --- a/frontend/src/app/pages/header/header/header.component.html +++ b/frontend/src/app/pages/header/header/header.component.html @@ -132,8 +132,8 @@ - + + diff --git a/frontend/src/app/pages/reference/nickgenerator/nick-generator.component.html b/frontend/src/app/pages/reference/nickgenerator/nick-generator.component.html index 8713313..8ac22a6 100644 --- a/frontend/src/app/pages/reference/nickgenerator/nick-generator.component.html +++ b/frontend/src/app/pages/reference/nickgenerator/nick-generator.component.html @@ -40,7 +40,7 @@ + [style.visibility]="(part.continuation && i>0 && parts[i-1].gradient && part.gradient) ? 'hidden' : 'visible'"> Color A Continuation diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index f33cfec..2efa26d 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -45,6 +45,9 @@ export class AuthService { } private reloadUsername() { + if (window.location.hostname === 'localhost') { + this._username.set('developer'); + } this.loginService.getUsername().subscribe({ next: (username) => { this._username.set(username.username); diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml index 9a5ccb9..e683976 100644 --- a/open_api/src/main/resources/api.yml +++ b/open_api/src/main/resources/api.yml @@ -59,6 +59,8 @@ paths: $ref: './schemas/bans/bans.yml#/removePunishment' /api/appeal/update-mail: $ref: './schemas/forms/appeal/appeal.yml#/UpdateMail' + /api/appeal/discord/getBannedUser: + $ref: './schemas/forms/appeal/discordAppeal.yml#/getBannedUser' /api/appeal/minecraft-appeal: $ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal' /api/appeal/discord-appeal: diff --git a/open_api/src/main/resources/schemas/forms/appeal/appeal.yml b/open_api/src/main/resources/schemas/forms/appeal/appeal.yml index 176c840..b40c1d0 100644 --- a/open_api/src/main/resources/schemas/forms/appeal/appeal.yml +++ b/open_api/src/main/resources/schemas/forms/appeal/appeal.yml @@ -119,23 +119,20 @@ components: type: object description: Schema for Discord ban/punishment appeals required: - - email - - punishmentId + - discordId - appeal + - email properties: - userId: + discordId: type: integer format: int64 - description: Discord user ID of the appealing user - email: - type: string - description: Contact email address of the appealing user - punishmentId: - type: integer - description: Unique identifier of the punishment being appealed + description: Discord user's unique identifier' appeal: type: string description: Appeal text explaining why the punishment should be reconsidered + email: + type: string + description: Contact email address of the appealing user UpdateMail: type: object required: diff --git a/open_api/src/main/resources/schemas/forms/appeal/discordAppeal.yml b/open_api/src/main/resources/schemas/forms/appeal/discordAppeal.yml new file mode 100644 index 0000000..a31a152 --- /dev/null +++ b/open_api/src/main/resources/schemas/forms/appeal/discordAppeal.yml @@ -0,0 +1,57 @@ +getBannedUser: + post: + tags: + - appeals + summary: get banned user + description: Get a banned user by their discord id + operationId: getBannedUser + parameters: + - name: discordId + in: query + required: true + description: The discord id of the user + schema: + type: integer + format: int64 + responses: + '200': + description: Banned user + content: + application/json: + schema: + $ref: '#/components/schemas/BannedUserResponse' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '../../generic/errors.yml#/components/schemas/ApiError' +components: + schemas: + BannedUserResponse: + type: object + required: + - isBanned + properties: + isBanned: + type: boolean + description: Whether the user is banned + bannedUser: + $ref: '#/components/schemas/BannedUser' + BannedUser: + type: object + required: + - userId + - reason + - name + - avatarUrl + properties: + userId: + type: integer + format: int64 + reason: + type: string + name: + type: string + avatarUrl: + type: string