diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java index 788ab4c..3024f2c 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java @@ -10,7 +10,9 @@ import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; import com.alttd.altitudeweb.mappers.StaffApplicationDataMapper; import com.alttd.altitudeweb.model.FormResponseDto; import com.alttd.altitudeweb.model.StaffApplicationDto; +import com.alttd.altitudeweb.services.discord.StaffApplicationDiscord; import com.alttd.altitudeweb.services.limits.RateLimit; +import com.alttd.altitudeweb.services.mail.StaffApplicationMail; import com.alttd.altitudeweb.setup.Connection; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,6 +31,8 @@ import java.util.concurrent.TimeUnit; public class ApplicationController implements ApplicationsApi { private final StaffApplicationDataMapper staffApplicationDataMapper; + private final StaffApplicationMail staffApplicationMail; + private final StaffApplicationDiscord staffApplicationDiscord; @Override public ResponseEntity submitStaffApplication(StaffApplicationDto staffApplicationDto) { @@ -40,8 +44,35 @@ public class ApplicationController implements ApplicationsApi { Optional optionalEmail = fetchEmailVerification(userUuid, application.email()); boolean verified = optionalEmail.map(EmailVerification::verified).orElse(false); + boolean success = true; if (verified) { - markAsSent(application.id()); + // Send mail first; only if sent, send to Discord, then mark as sent + boolean mailSent = false; + try { + mailSent = staffApplicationMail.sendApplicationEmail(application); + } catch (Exception e) { + log.error("Error while sending staff application email for {}", application.id(), e); + success = false; + } + + if (mailSent) { + try { + staffApplicationDiscord.sendApplicationToDiscord(application); + } catch (Exception e) { + log.error("Failed to send staff application {} to Discord", application.id(), e); + success = false; + } + } else { + success = false; + } + + if (success) { + markAsSent(application.id()); + } + } + + if (verified && !success) { + return ResponseEntity.internalServerError().build(); } FormResponseDto response = buildResponse(application, verified); diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java b/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java new file mode 100644 index 0000000..cbbc624 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java @@ -0,0 +1,103 @@ +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.web_db.forms.StaffApplication; +import com.alttd.altitudeweb.setup.Connection; +import com.alttd.webinterface.send_message.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.concurrent.CompletableFuture; + +@Slf4j +@Service +public class StaffApplicationDiscord { + + private static final String OUTPUT_TYPE = "STAFF_APPLICATION"; + + public void sendApplicationToDiscord(StaffApplication application) { + // Fetch channels for staff applications + 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<>()); + } + }); + + List channels = channelsFuture.join(); + if (channels.isEmpty()) { + log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE); + return; + } + + // Build embed content + List fields = new ArrayList<>(); + fields.add(new DiscordSender.EmbedField( + "Applicant", + "UUID: " + safe(String.valueOf(application.uuid())) + "\n" + + "Discord: " + safe(application.discordUsername()) + "\n" + + "Email: " + safe(application.email()) + "\n" + + "Age: " + safe(String.valueOf(application.age())) + "\n" + + "Meets reqs: " + (application.meetsRequirements() != null && application.meetsRequirements()), + false + )); + fields.add(new DiscordSender.EmbedField( + "Availability", + "Days: " + safe(application.availableDays()) + "\n" + + "Times: " + safe(application.availableTimes()), + false + )); + fields.add(new DiscordSender.EmbedField( + "Experience", + "Previous: " + safe(application.previousExperience()) + "\n" + + "Plugins: " + safe(application.pluginExperience()) + "\n" + + "Expectations: " + safe(application.moderatorExpectations()), + false + )); + if (application.additionalInfo() != null && !application.additionalInfo().isBlank()) { + fields.add(new DiscordSender.EmbedField( + "Additional Info", + application.additionalInfo(), + false + )); + } + + List channelIds = channels.stream() + .map(OutputChannel::channel) + .toList(); + + Instant timestamp = application.createdAt() != null ? application.createdAt() : Instant.now(); + DiscordSender.getInstance().sendEmbedToChannels( + channelIds, + "New Staff Application Submitted", + "Join date: " + (application.joinDate() != null ? application.joinDate().toString() : "unknown") + + "\nSubmitted: " + formatInstant(timestamp), + fields, + null, + timestamp, + null + ); + } + + private String safe(String s) { + return s == null ? "unknown" : s; + } + + 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)'")); + } +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/mail/StaffApplicationMail.java b/backend/src/main/java/com/alttd/altitudeweb/services/mail/StaffApplicationMail.java new file mode 100644 index 0000000..7b9b6c4 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/services/mail/StaffApplicationMail.java @@ -0,0 +1,71 @@ +package com.alttd.altitudeweb.services.mail; + +import com.alttd.altitudeweb.database.web_db.forms.StaffApplication; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StaffApplicationMail { + + private final JavaMailSender mailSender; + private final SpringTemplateEngine templateEngine; + + @Value("${spring.mail.username}") + private String fromEmail; + + private static final String STAFF_APPLICATION_EMAIL = "staff@alttd.com"; + + /** + * Sends an email with the staff application details to the staff applications team mailbox. + * Returns true if the email was sent successfully. + */ + public boolean sendApplicationEmail(StaffApplication application) { + try { + doSend(application); + log.info("Staff application email sent successfully for application ID: {}", application.id()); + return true; + } catch (Exception e) { + log.error("Failed to send staff application email for application ID: {}", application.id(), e); + return false; + } + } + + private void doSend(StaffApplication application) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + helper.setFrom(fromEmail); + helper.setTo(STAFF_APPLICATION_EMAIL); + helper.setReplyTo(application.email()); + helper.setSubject("New Staff Application - " + safe(application.discordUsername())); + + // Prepare template context + String createdAt = application.createdAt() + .atZone(ZoneId.of("UTC")) + .format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'")); + Context context = new Context(); + context.setVariable("application", application); + context.setVariable("createdAt", createdAt); + + String content = templateEngine.process("staff-application-email", context); + helper.setText(content, true); + mailSender.send(message); + } + + private String safe(String s) { + return s == null ? "unknown" : s; + } +} diff --git a/backend/src/main/resources/templates/staff-application-email.html b/backend/src/main/resources/templates/staff-application-email.html new file mode 100644 index 0000000..8227678 --- /dev/null +++ b/backend/src/main/resources/templates/staff-application-email.html @@ -0,0 +1,138 @@ + + + + + Staff Application + + + +
+ The Altitude Minecraft Server +

Staff application

+ +
+
+
+

Applicant

+
    +
  • UUID: uuid
  • +
  • Email: email
  • +
  • Discord: discord
  • +
  • Age: age
  • +
  • Pronouns: pronouns
  • +
  • Join date: date
  • +
  • Submitted at: date
  • +
+
+
+
+
+

Availability

+
    +
  • Days: days
  • +
  • Times: times
  • +
  • Weekly playtime: 0 hours
  • +
+ +

Experience

+

Previous:
previous experience

+

Plugins:
plugin experience

+

Expectations:
moderator expectations

+ +
+

Additional info

+

additional info

+
+
+
+
+
+ +