diff --git a/README.md b/README.md
index 2e83dfa..1f42f3d 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ and deliver them to a configured channel without the need to manually navigate t
With support for detecting more than 72 events, it can log changes made to: AutoMod Settings, Servers, Onboarding,
Invites, Members, Roles, Channels, Threads, Stages, Events, Polls, Messages, Boosts, Emojis, Stickers, Soundboard,
-Integrations, Webhooks, Moderation Action and Unknown events.
+Integrations, Webhooks, Moderation Action, Unusual DMs, Raids and Unknown events.
> Get it from here: https://discord.com/discovery/applications/1381658412550590475
diff --git a/pom.xml b/pom.xml
index 66576fb..dbfed1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.github.eggy03
papertrailbot
- 4.0.0
+ 4.1.0
PaperTrail Bot
A simple bot for logging all server and member actions
quarkus
diff --git a/src/main/java/io/github/eggy03/papertrail/bot/constant/ProjectInfo.java b/src/main/java/io/github/eggy03/papertrail/bot/constant/ProjectInfo.java
index c071194..73d4b8c 100644
--- a/src/main/java/io/github/eggy03/papertrail/bot/constant/ProjectInfo.java
+++ b/src/main/java/io/github/eggy03/papertrail/bot/constant/ProjectInfo.java
@@ -10,7 +10,7 @@ public final class ProjectInfo {
public static final String APPNAME = "PaperTrail";
@NonNull
- public static final String VERSION = "v4.0.0";
+ public static final String VERSION = "v4.1.0";
@NonNull
public static final String PROJECT_ISSUE_LINK = "https://github.com/eggy03/PaperTrailBot/issues";
diff --git a/src/main/java/io/github/eggy03/papertrail/bot/handlers/guild/GuildSecurityIncidentEventHandler.java b/src/main/java/io/github/eggy03/papertrail/bot/handlers/guild/GuildSecurityIncidentEventHandler.java
new file mode 100644
index 0000000..160141e
--- /dev/null
+++ b/src/main/java/io/github/eggy03/papertrail/bot/handlers/guild/GuildSecurityIncidentEventHandler.java
@@ -0,0 +1,182 @@
+package io.github.eggy03.papertrail.bot.handlers.guild;
+
+import io.github.eggy03.papertrail.sdk.client.AuditLogRegistrationClient;
+import io.github.eggy03.papertrail.sdk.entity.AuditLogRegistrationEntity;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.guild.SecurityIncidentActions;
+import net.dv8tion.jda.api.entities.guild.SecurityIncidentDetections;
+import net.dv8tion.jda.api.events.guild.update.GenericGuildUpdateEvent;
+import net.dv8tion.jda.api.events.guild.update.GuildUpdateSecurityIncidentActionsEvent;
+import net.dv8tion.jda.api.events.guild.update.GuildUpdateSecurityIncidentDetectionsEvent;
+import net.dv8tion.jda.api.utils.MarkdownUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.jspecify.annotations.Nullable;
+
+import java.awt.Color;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+
+@ApplicationScoped
+@Slf4j
+public final class GuildSecurityIncidentEventHandler {
+
+ private final @NonNull AuditLogRegistrationClient client;
+
+ @Inject
+ public GuildSecurityIncidentEventHandler(@NonNull AuditLogRegistrationClient client) {
+ this.client = client;
+ }
+
+ @NonNull
+ private String getRegisteredGuildChannel(@NonNull String guildId) {
+ return client.getRegisteredGuild(guildId)
+ .map(AuditLogRegistrationEntity::getChannelId).orElse(StringUtils.EMPTY);
+
+ }
+
+ private void performChecksThenBuildAndSendEmbed(@NonNull GenericGuildUpdateEvent> event, @NonNull EmbedBuilder embedBuilder, @NonNull String channelIdToSendTo) {
+ if (!embedBuilder.isValidLength() || embedBuilder.isEmpty()) {
+ log.warn("Embed is empty or too long (current length: {}).", embedBuilder.length());
+ return;
+ }
+
+ TextChannel sendingChannel = event.getGuild().getTextChannelById(channelIdToSendTo);
+ if (sendingChannel != null && sendingChannel.canTalk()) {
+ sendingChannel.sendMessageEmbeds(embedBuilder.build()).queue();
+ }
+ }
+
+ private @NonNull String formatOffsetDateTime(@Nullable OffsetDateTime offsetDateTime) {
+ if (offsetDateTime == null) return "N/A";
+ return "";
+ }
+
+ public void handleGuildUpdateSecurityIncidentDetections(@NonNull GuildUpdateSecurityIncidentDetectionsEvent event) {
+ String channelIdToSendTo = getRegisteredGuildChannel(event.getGuild().getId());
+ if (channelIdToSendTo.isBlank()) return;
+
+ SecurityIncidentDetections oldSID = event.getOldSecurityIncidentDetections();
+ SecurityIncidentDetections newSID = event.getNewSecurityIncidentDetections();
+
+ OffsetDateTime oldDMSpamDetectedAt = oldSID.getTimeDetectedDmSpam();
+ OffsetDateTime newDMSpamDetectedAt = newSID.getTimeDetectedDmSpam();
+
+ OffsetDateTime oldRaidDetectedAt = oldSID.getTimeDetectedRaid();
+ OffsetDateTime newRaidDetectedAt = newSID.getTimeDetectedRaid();
+
+ if (oldDMSpamDetectedAt != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Incident Resolved");
+ eb.setDescription(MarkdownUtil.quoteBlock("Incident Type: DM Spam\nStatus: Resolved"));
+ eb.setColor(Color.GREEN);
+ eb.addField(MarkdownUtil.underline("DM Spam Started At"), formatOffsetDateTime(oldDMSpamDetectedAt), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (newDMSpamDetectedAt != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Incident Detected");
+ eb.setDescription(MarkdownUtil.quoteBlock("Incident Type: DM Spam\nStatus: Detected"));
+ eb.setColor(Color.ORANGE);
+ eb.addField(MarkdownUtil.underline("DM Spam Detected At"), formatOffsetDateTime(newDMSpamDetectedAt), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (oldRaidDetectedAt != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Incident Resolved");
+ eb.setDescription(MarkdownUtil.quoteBlock("Incident Type: RAID\nStatus: Resolved"));
+ eb.setColor(Color.GREEN);
+ eb.addField(MarkdownUtil.underline("Raid Was Detected At"), formatOffsetDateTime(oldRaidDetectedAt), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (newRaidDetectedAt != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Incident Detected");
+ eb.setDescription(MarkdownUtil.quoteBlock("Incident Type: RAID\nStatus: Detected"));
+ eb.setColor(Color.ORANGE);
+ eb.addField(MarkdownUtil.underline("Raid Detected At"), formatOffsetDateTime(newRaidDetectedAt), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+ }
+
+ public void handleGuildUpdateSecurityIncidentActions(@NonNull GuildUpdateSecurityIncidentActionsEvent event) {
+ String channelIdToSendTo = getRegisteredGuildChannel(event.getGuild().getId());
+ if (channelIdToSendTo.isBlank()) return;
+
+ SecurityIncidentActions oldSIA = event.getOldSecurityIncidentActions();
+ SecurityIncidentActions newSIA = event.getNewSecurityIncidentActions();
+
+ OffsetDateTime oldDMDisabledUntil = oldSIA.getDirectMessagesDisabledUntil();
+ OffsetDateTime newDMDisabledUntil = newSIA.getDirectMessagesDisabledUntil();
+
+ OffsetDateTime oldInvitesPausedUntil = oldSIA.getInvitesDisabledUntil();
+ OffsetDateTime newInvitesPausedUntil = newSIA.getInvitesDisabledUntil();
+
+ if (oldDMDisabledUntil != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Action Disabled");
+ eb.setDescription(MarkdownUtil.quoteBlock("Action Type: DM Pause\nStatus: Disabled"));
+ eb.setColor(Color.GREEN);
+ eb.addField(MarkdownUtil.underline("DMs Were Paused Until"), formatOffsetDateTime(oldDMDisabledUntil), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (newDMDisabledUntil != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Action Enabled");
+ eb.setDescription(MarkdownUtil.quoteBlock("Action Type: DM Pause\nStatus: Enabled"));
+ eb.setColor(Color.ORANGE);
+ eb.addField(MarkdownUtil.underline("DMs Paused Until"), formatOffsetDateTime(newDMDisabledUntil), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (oldInvitesPausedUntil != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Action Disabled");
+ eb.setDescription(MarkdownUtil.quoteBlock("Action Type: Invite Pause\nStatus: Disabled"));
+ eb.setColor(Color.GREEN);
+ eb.addField(MarkdownUtil.underline("Invites Were Paused Until"), formatOffsetDateTime(oldInvitesPausedUntil), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+
+ if (newInvitesPausedUntil != null) {
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setTitle("Audit Log Entry | Security Action Enabled");
+ eb.setDescription(MarkdownUtil.quoteBlock("Action Type: Invite Pause\nStatus: Enabled"));
+ eb.setColor(Color.ORANGE);
+ eb.addField(MarkdownUtil.underline("Invites Paused Until"), formatOffsetDateTime(newInvitesPausedUntil), false);
+ eb.setFooter(event.getGuild().getName());
+ eb.setTimestamp(Instant.now());
+
+ performChecksThenBuildAndSendEmbed(event, eb, channelIdToSendTo);
+ }
+ }
+}
diff --git a/src/main/java/io/github/eggy03/papertrail/bot/listeners/guild/GuildSecurityIncidentEventListener.java b/src/main/java/io/github/eggy03/papertrail/bot/listeners/guild/GuildSecurityIncidentEventListener.java
new file mode 100644
index 0000000..c576452
--- /dev/null
+++ b/src/main/java/io/github/eggy03/papertrail/bot/listeners/guild/GuildSecurityIncidentEventListener.java
@@ -0,0 +1,31 @@
+package io.github.eggy03.papertrail.bot.listeners.guild;
+
+import io.github.eggy03.papertrail.bot.handlers.guild.GuildSecurityIncidentEventHandler;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import lombok.NonNull;
+import net.dv8tion.jda.api.events.guild.update.GuildUpdateSecurityIncidentActionsEvent;
+import net.dv8tion.jda.api.events.guild.update.GuildUpdateSecurityIncidentDetectionsEvent;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+
+@ApplicationScoped
+public final class GuildSecurityIncidentEventListener extends ListenerAdapter {
+
+ private final @NonNull GuildSecurityIncidentEventHandler handler;
+
+ @Inject
+ public GuildSecurityIncidentEventListener(@NonNull GuildSecurityIncidentEventHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void onGuildUpdateSecurityIncidentDetections(@NonNull GuildUpdateSecurityIncidentDetectionsEvent event) {
+ handler.handleGuildUpdateSecurityIncidentDetections(event);
+ }
+
+ @Override
+ public void onGuildUpdateSecurityIncidentActions(@NonNull GuildUpdateSecurityIncidentActionsEvent event) {
+ handler.handleGuildUpdateSecurityIncidentActions(event);
+ }
+
+}