diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/resources/react_mappings.json b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/resources/react_mappings.json
index d3c421ed6..9e6333e6a 100644
--- a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/resources/react_mappings.json
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/resources/react_mappings.json
@@ -119,7 +119,8 @@
"♌"
],
"r": [
- "🇷"
+ "🇷",
+ "🌱"
],
"s": [
"🇸",
diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/ExperienceLevelServiceBean.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/ExperienceLevelServiceBean.java
index e3e6ee2bd..364cae35f 100644
--- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/ExperienceLevelServiceBean.java
+++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/ExperienceLevelServiceBean.java
@@ -57,7 +57,8 @@ public class ExperienceLevelServiceBean implements ExperienceLevelService {
@Override
public Long calculateExperienceToNextLevel(Integer level, Long currentExperience) {
- AExperienceLevel nextLevel = experienceLevelManagementService.getLevel(level + 1).orElseThrow(() -> new AbstractoRunTimeException(String.format("Could not find level %s", level)));
+ AExperienceLevel nextLevel = experienceLevelManagementService.getLevel(level + 1)
+ .orElseThrow(() -> new AbstractoRunTimeException(String.format("Could not find level %s", level)));
return nextLevel.getExperienceNeeded() - currentExperience;
}
diff --git a/abstracto-application/abstracto-modules/invite-filter/invite-filter-impl/src/main/java/dev/sheldan/abstracto/invitefilter/listener/InviteLinkFilterListener.java b/abstracto-application/abstracto-modules/invite-filter/invite-filter-impl/src/main/java/dev/sheldan/abstracto/invitefilter/listener/InviteLinkFilterListener.java
index 917949aeb..a77c21bfc 100644
--- a/abstracto-application/abstracto-modules/invite-filter/invite-filter-impl/src/main/java/dev/sheldan/abstracto/invitefilter/listener/InviteLinkFilterListener.java
+++ b/abstracto-application/abstracto-modules/invite-filter/invite-filter-impl/src/main/java/dev/sheldan/abstracto/invitefilter/listener/InviteLinkFilterListener.java
@@ -9,7 +9,6 @@ import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.*;
-import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
@@ -25,7 +24,6 @@ import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Invite;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
-import net.dv8tion.jda.api.entities.MessageType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -67,14 +65,14 @@ public class InviteLinkFilterListener implements AsyncMessageReceivedListener {
@Autowired
private RoleImmunityService roleImmunityService;
- public static final String MODERATION_PURGE_METRIC = "invite.filter";
+ public static final String INVITE_FILTER_METRIC = "invite.filter";
public static final String CONSEQUENCE = "consequence";
private static final CounterMetric MESSAGE_INVITE_FILTERED =
CounterMetric
.builder()
.tagList(Arrays.asList(MetricTag.getTag(CONSEQUENCE, "filtered")))
- .name(MODERATION_PURGE_METRIC)
+ .name(INVITE_FILTER_METRIC)
.build();
public static final String INVITE_LINK_DELETED_NOTIFICATION_EMBED_TEMPLATE_KEY = "invite_link_deleted_notification";
diff --git a/abstracto-application/abstracto-modules/pom.xml b/abstracto-application/abstracto-modules/pom.xml
index 950a4ce50..29664a2e4 100644
--- a/abstracto-application/abstracto-modules/pom.xml
+++ b/abstracto-application/abstracto-modules/pom.xml
@@ -26,7 +26,7 @@
webservices
logging
invite-filter
+ profanity-filter
-
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/pom.xml b/abstracto-application/abstracto-modules/profanity-filter/pom.xml
new file mode 100644
index 000000000..693b6ed7e
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/pom.xml
@@ -0,0 +1,19 @@
+
+
+
+ abstracto-modules
+ dev.sheldan.abstracto.modules
+ 1.2.12-SNAPSHOT
+
+ 4.0.0
+
+ profanity-filter
+ pom
+
+ profanity-filter-int
+ profanity-filter-impl
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/pom.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/pom.xml
new file mode 100644
index 000000000..5d927f4f2
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ profanity-filter
+ dev.sheldan.abstracto.modules
+ 1.2.12-SNAPSHOT
+
+ 4.0.0
+
+ profanity-filter-impl
+
+
+ 8
+ 8
+
+
+
+
+
+ maven-assembly-plugin
+
+
+ src/main/assembly/liquibase.xml
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ dev.sheldan.abstracto.modules
+ profanity-filter-int
+ ${project.version}
+
+
+
+ dev.sheldan.abstracto.core
+ metrics-int
+ ${project.version}
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/assembly/liquibase.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/assembly/liquibase.xml
new file mode 100644
index 000000000..8b4774fa0
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/assembly/liquibase.xml
@@ -0,0 +1,18 @@
+
+ liquibase
+
+ zip
+
+ false
+
+
+ .
+ ${project.basedir}/src/main/resources/migrations
+
+ **/*
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/command/Profanities.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/command/Profanities.java
new file mode 100644
index 000000000..4687ebf00
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/command/Profanities.java
@@ -0,0 +1,93 @@
+package dev.sheldan.abstracto.profanityfilter.command;
+
+import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
+import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
+import dev.sheldan.abstracto.core.command.config.HelpInfo;
+import dev.sheldan.abstracto.core.command.config.Parameter;
+import dev.sheldan.abstracto.core.command.execution.CommandContext;
+import dev.sheldan.abstracto.core.command.execution.CommandResult;
+import dev.sheldan.abstracto.core.config.FeatureDefinition;
+import dev.sheldan.abstracto.core.config.FeatureMode;
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.service.ChannelService;
+import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
+import dev.sheldan.abstracto.core.utils.FutureUtils;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterFeatureDefinition;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterMode;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterModerationModuleDefinition;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import dev.sheldan.abstracto.profanityfilter.model.template.ProfanitiesModel;
+import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService;
+import dev.sheldan.abstracto.profanityfilter.service.management.ProfanityUserInServerManagementService;
+import net.dv8tion.jda.api.entities.Member;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+@Component
+public class Profanities extends AbstractConditionableCommand {
+
+ @Autowired
+ private ProfanityFilterService profanityFilterService;
+
+ @Autowired
+ private UserInServerManagementService userInServerManagementService;
+
+ @Autowired
+ private ProfanityUserInServerManagementService profanityUserInServerManagementService;
+
+ @Autowired
+ private ChannelService channelService;
+
+ private static final String PROFANITIES_TEMPLATE_KEY = "profanities_response";
+
+ @Override
+ public CompletableFuture executeAsync(CommandContext commandContext) {
+ Member member = (Member) commandContext.getParameters().getParameters().get(0);
+ AUserInAServer userInServer = userInServerManagementService.loadOrCreateUser(member);
+ ProfanityUserInAServer profanityUser = profanityUserInServerManagementService.getProfanityUser(userInServer);
+ Long positiveReports = profanityFilterService.getPositiveReportCountForUser(profanityUser);
+ Long falsePositives = profanityFilterService.getFalseProfanityReportCountForUser(profanityUser);
+ List reports = profanityFilterService.getRecentPositiveReports(profanityUser, 3);
+ ProfanitiesModel model = ProfanitiesModel
+ .builder()
+ .member(member)
+ .recentPositiveReports(reports)
+ .falsePositives(falsePositives)
+ .truePositives(positiveReports)
+ .build();
+ return FutureUtils.toSingleFutureGeneric(channelService.sendEmbedTemplateInTextChannelList(PROFANITIES_TEMPLATE_KEY, model, commandContext.getChannel()))
+ .thenApply(unused -> CommandResult.fromSuccess());
+ }
+
+ @Override
+ public CommandConfiguration getConfiguration() {
+ Parameter memberParameter = Parameter.builder().templated(true).name("member").type(Member.class).optional(true).build();
+ List parameters = Collections.singletonList(memberParameter);
+ HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
+ return CommandConfiguration.builder()
+ .name("profanities")
+ .module(ProfanityFilterModerationModuleDefinition.MODERATION)
+ .templated(true)
+ .async(true)
+ .causesReaction(false)
+ .parameters(parameters)
+ .help(helpInfo)
+ .build();
+ }
+
+ @Override
+ public FeatureDefinition getFeature() {
+ return ProfanityFilterFeatureDefinition.PROFANITY_FILTER;
+ }
+
+ @Override
+ public List getFeatureModeLimitations() {
+ return Arrays.asList(ProfanityFilterMode.TRACK_PROFANITIES);
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterProperties.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterProperties.java
new file mode 100644
index 000000000..79346823d
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterProperties.java
@@ -0,0 +1,9 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@PropertySource("classpath:profanityFilter-config.properties")
+public class ProfanityFilterProperties {
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityDetectionListener.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityDetectionListener.java
new file mode 100644
index 000000000..1d61b0914
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityDetectionListener.java
@@ -0,0 +1,107 @@
+package dev.sheldan.abstracto.profanityfilter.listener;
+
+import dev.sheldan.abstracto.core.config.FeatureDefinition;
+import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
+import dev.sheldan.abstracto.core.listener.async.jda.AsyncMessageReceivedListener;
+import dev.sheldan.abstracto.core.metric.service.CounterMetric;
+import dev.sheldan.abstracto.core.metric.service.MetricService;
+import dev.sheldan.abstracto.core.metric.service.MetricTag;
+import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
+import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
+import dev.sheldan.abstracto.core.service.FeatureModeService;
+import dev.sheldan.abstracto.core.service.MessageService;
+import dev.sheldan.abstracto.core.service.ProfanityService;
+import dev.sheldan.abstracto.core.service.RoleImmunityService;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterFeatureDefinition;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterMode;
+import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService;
+import lombok.extern.slf4j.Slf4j;
+import net.dv8tion.jda.api.entities.Message;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.Arrays;
+import java.util.Optional;
+
+import static dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService.PROFANITY_FILTER_EFFECT_KEY;
+
+@Component
+@Slf4j
+public class ProfanityDetectionListener implements AsyncMessageReceivedListener {
+
+ @Autowired
+ private ProfanityService profanityService;
+
+ @Autowired
+ private ProfanityFilterService profanityFilterService;
+
+ @Autowired
+ private FeatureModeService featureModeService;
+
+ @Autowired
+ private MessageService messageService;
+
+ @Autowired
+ private MetricService metricService;
+
+ @Autowired
+ private RoleImmunityService roleImmunityService;
+
+ public static final String MODERATION_PURGE_METRIC = "profanity.filter";
+ public static final String STEP = "step";
+
+ private static final CounterMetric PROFANITIES_DETECTED_METRIC =
+ CounterMetric
+ .builder()
+ .tagList(Arrays.asList(MetricTag.getTag(STEP, "detection")))
+ .name(MODERATION_PURGE_METRIC)
+ .build();
+
+ @Override
+ public DefaultListenerResult execute(MessageReceivedModel model) {
+ Message message = model.getMessage();
+ if(message.isWebhookMessage() || message.getType().isSystem() || !message.isFromGuild()) {
+ return DefaultListenerResult.IGNORED;
+ }
+
+ if(roleImmunityService.isImmune(message.getMember(), PROFANITY_FILTER_EFFECT_KEY)) {
+ log.info("Not checking for profanities in message, because author {} in channel {} in guild {} is immune against profanity filter.",
+ message.getMember().getIdLong(), message.getGuild().getIdLong(), message.getChannel().getIdLong());
+ return DefaultListenerResult.IGNORED;
+ }
+
+ Long serverId = model.getServerId();
+ Optional potentialProfanityGroup = profanityService.getProfanityRegex(message.getContentRaw(), serverId);
+ if(potentialProfanityGroup.isPresent()) {
+ metricService.incrementCounter(PROFANITIES_DETECTED_METRIC);
+ if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, serverId, ProfanityFilterMode.PROFANITY_REPORT)) {
+ ProfanityRegex foundProfanityGroup = potentialProfanityGroup.get();
+ profanityFilterService.createProfanityReport(message, foundProfanityGroup).exceptionally(throwable -> {
+ log.error("Failed to report or persist profanities in server {} for message {} in channel {}.",
+ serverId, message.getChannel().getIdLong(), message.getIdLong(), throwable);
+ return null;
+ });
+ }
+ if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, serverId, ProfanityFilterMode.AUTO_DELETE_PROFANITIES)) {
+ messageService.deleteMessage(message).exceptionally(throwable -> {
+ log.error("Failed to delete profanity message with id {} in channel {} in server {}.",
+ message.getIdLong(), message.getChannel().getIdLong(), message.getGuild().getIdLong(), throwable);
+ return null;
+ });
+ }
+ return DefaultListenerResult.PROCESSED;
+ }
+ return DefaultListenerResult.IGNORED;
+ }
+
+ @Override
+ public FeatureDefinition getFeature() {
+ return ProfanityFilterFeatureDefinition.PROFANITY_FILTER;
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ metricService.registerCounter(PROFANITIES_DETECTED_METRIC, "Amount of profanities detected");
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityReportVoteListener.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityReportVoteListener.java
new file mode 100644
index 000000000..e9155d925
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/listener/ProfanityReportVoteListener.java
@@ -0,0 +1,100 @@
+package dev.sheldan.abstracto.profanityfilter.listener;
+
+import dev.sheldan.abstracto.core.config.FeatureDefinition;
+import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
+import dev.sheldan.abstracto.core.listener.async.jda.AsyncReactionAddedListener;
+import dev.sheldan.abstracto.core.models.cache.CachedMessage;
+import dev.sheldan.abstracto.core.models.cache.CachedReactions;
+import dev.sheldan.abstracto.core.models.database.AEmote;
+import dev.sheldan.abstracto.core.models.listener.ReactionAddedModel;
+import dev.sheldan.abstracto.core.service.ConfigService;
+import dev.sheldan.abstracto.core.service.EmoteService;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterFeatureDefinition;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService;
+import dev.sheldan.abstracto.profanityfilter.service.management.ProfanityUseManagementService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+import static dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService.PROFANITY_VOTES_CONFIG_KEY;
+
+@Component
+public class ProfanityReportVoteListener implements AsyncReactionAddedListener {
+
+ @Autowired
+ private EmoteService emoteService;
+
+ @Autowired
+ private ProfanityUseManagementService profanityUseManagementService;
+
+ @Autowired
+ private ConfigService configService;
+
+ @Autowired
+ private ProfanityFilterService profanityFilterService;
+
+ @Override
+ public DefaultListenerResult execute(ReactionAddedModel model) {
+ Optional profanityUseOptional = profanityUseManagementService.getProfanityUseViaReportMessageId(model.getMessage().getMessageId());
+ if(profanityUseOptional.isPresent()) {
+ ProfanityUse use = profanityUseOptional.get();
+ if(use.getVerified()) {
+ return DefaultListenerResult.PROCESSED;
+ }
+ AEmote addedEmote = emoteService.buildAEmoteFromReaction(model.getReaction().getReactionEmote());
+ AEmote agreeEmote = emoteService.getEmoteOrDefaultEmote(ProfanityFilterService.REPORT_AGREE_EMOTE, model.getServerId());
+ boolean isAgreement = emoteService.compareAEmote(addedEmote, agreeEmote);
+ boolean reactionWasVote;
+ AEmote disApproveEmote = emoteService.getEmoteOrDefaultEmote(ProfanityFilterService.REPORT_DISAGREE_EMOTE, model.getServerId());
+ if(!isAgreement) {
+ reactionWasVote = emoteService.compareAEmote(addedEmote, disApproveEmote);
+ } else {
+ reactionWasVote = true;
+ }
+ if(reactionWasVote) {
+ ProfanityFilterService.VoteResult voteResult = getVoteResultOnMessage(model.getMessage(), agreeEmote, disApproveEmote);
+ if(ProfanityFilterService.VoteResult.isFinal(voteResult)) {
+ profanityFilterService.verifyProfanityUse(use, voteResult);
+ }
+ }
+ }
+ return DefaultListenerResult.IGNORED;
+ }
+
+ private ProfanityFilterService.VoteResult getVoteResultOnMessage(CachedMessage cachedMessage, AEmote agreementEmote, AEmote disagreementEmote) {
+ Long voteThreshold = configService.getLongValueOrConfigDefault(PROFANITY_VOTES_CONFIG_KEY, cachedMessage.getServerId());
+ Optional agreementReactionsOptional = emoteService.getReactionFromMessageByEmote(cachedMessage, agreementEmote);
+ Optional disAgreementReactionsOptional = emoteService.getReactionFromMessageByEmote(cachedMessage, disagreementEmote);
+ int agreementVotes = 0;
+ int disagreementVotes = 0;
+ if(agreementReactionsOptional.isPresent()) {
+ agreementVotes = getUserCount(agreementReactionsOptional.get());
+ }
+ if(disAgreementReactionsOptional.isPresent()) {
+ disagreementVotes = getUserCount(disAgreementReactionsOptional.get());
+ }
+ if(agreementVotes >= voteThreshold) {
+ return ProfanityFilterService.VoteResult.AGREEMENT;
+ } else if(disagreementVotes >= voteThreshold) {
+ return ProfanityFilterService.VoteResult.DISAGREEMENT;
+ } else {
+ return ProfanityFilterService.VoteResult.BELOW_THRESHOLD;
+ }
+ }
+
+ private int getUserCount(CachedReactions agreementReactionsOptional) {
+ int reactionCount = agreementReactionsOptional.getUsers().size();
+ if(agreementReactionsOptional.getSelf()) {
+ reactionCount--;
+ }
+ return reactionCount;
+ }
+
+ @Override
+ public FeatureDefinition getFeature() {
+ return ProfanityFilterFeatureDefinition.PROFANITY_FILTER;
+ }
+
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUseRepository.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUseRepository.java
new file mode 100644
index 000000000..c044fbe6e
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUseRepository.java
@@ -0,0 +1,16 @@
+package dev.sheldan.abstracto.profanityfilter.repository;
+
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ProfanityUseRepository extends JpaRepository {
+ Long countByProfanityUserAndVerifiedTrueAndConfirmedTrue(ProfanityUserInAServer profanityUserInAServer);
+ Long countByProfanityUserAndVerifiedTrueAndConfirmedFalse(ProfanityUserInAServer profanityUserInAServer);
+ List findAllByProfanityUserAndConfirmedTrueOrderByCreatedDesc(ProfanityUserInAServer profanityUserInAServer, Pageable pageable);
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUserInServerRepository.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUserInServerRepository.java
new file mode 100644
index 000000000..5303886b2
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/repository/ProfanityUserInServerRepository.java
@@ -0,0 +1,9 @@
+package dev.sheldan.abstracto.profanityfilter.repository;
+
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ProfanityUserInServerRepository extends JpaRepository {
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterServiceBean.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterServiceBean.java
new file mode 100644
index 000000000..7537160f6
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterServiceBean.java
@@ -0,0 +1,210 @@
+package dev.sheldan.abstracto.profanityfilter.service;
+
+import dev.sheldan.abstracto.core.metric.service.CounterMetric;
+import dev.sheldan.abstracto.core.metric.service.MetricService;
+import dev.sheldan.abstracto.core.metric.service.MetricTag;
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
+import dev.sheldan.abstracto.core.service.FeatureModeService;
+import dev.sheldan.abstracto.core.service.MessageService;
+import dev.sheldan.abstracto.core.service.PostTargetService;
+import dev.sheldan.abstracto.core.service.ReactionService;
+import dev.sheldan.abstracto.core.service.management.ProfanityRegexManagementService;
+import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
+import dev.sheldan.abstracto.core.templating.model.MessageToSend;
+import dev.sheldan.abstracto.core.templating.service.TemplateService;
+import dev.sheldan.abstracto.core.utils.FutureUtils;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterFeatureDefinition;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterMode;
+import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterPostTarget;
+import dev.sheldan.abstracto.profanityfilter.listener.ProfanityDetectionListener;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import dev.sheldan.abstracto.profanityfilter.model.template.ProfanityReportModel;
+import dev.sheldan.abstracto.profanityfilter.service.management.ProfanityUseManagementService;
+import dev.sheldan.abstracto.profanityfilter.service.management.ProfanityUserInServerManagementService;
+import lombok.extern.slf4j.Slf4j;
+import net.dv8tion.jda.api.entities.Message;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class ProfanityFilterServiceBean implements ProfanityFilterService {
+
+ @Autowired
+ private PostTargetService postTargetService;
+
+ @Autowired
+ private TemplateService templateService;
+
+ @Autowired
+ private ReactionService reactionService;
+
+ @Autowired
+ private ProfanityUserInServerManagementService profanityUserInServerManagementService;
+
+ @Autowired
+ private ProfanityUseManagementService profanityUseManagementService;
+
+ @Autowired
+ private ProfanityRegexManagementService profanityRegexManagementService;
+
+ @Autowired
+ private UserInServerManagementService userInServerManagementService;
+
+ @Autowired
+ private FeatureModeService featureModeService;
+
+ @Autowired
+ private MessageService messageService;
+
+ @Autowired
+ private MetricService metricService;
+
+ @Autowired
+ private ProfanityFilterServiceBean self;
+
+ private static final String PROFANITY_REPORT_TEMPLATE_KEY = "profanityDetection_listener_report";
+
+ private static final CounterMetric PROFANITIES_AGREEMENT =
+ CounterMetric
+ .builder()
+ .tagList(Arrays.asList(MetricTag.getTag(ProfanityDetectionListener.STEP, "agreement")))
+ .name(ProfanityDetectionListener.MODERATION_PURGE_METRIC)
+ .build();
+
+ private static final CounterMetric PROFANITIES_DISAGREEMENT =
+ CounterMetric
+ .builder()
+ .tagList(Arrays.asList(MetricTag.getTag(ProfanityDetectionListener.STEP, "disagreement")))
+ .name(ProfanityDetectionListener.MODERATION_PURGE_METRIC)
+ .build();
+
+ @Override
+ public CompletableFuture createProfanityReport(Message message, ProfanityRegex foundProfanityRegex) {
+ ProfanityReportModel reportModel = ProfanityReportModel
+ .builder()
+ .profaneMessage(message)
+ .profanityGroupKey(foundProfanityRegex.getGroup().getGroupName())
+ .profanityRegexName(foundProfanityRegex.getRegexName())
+ .build();
+ Long serverId = message.getGuild().getIdLong();
+ MessageToSend messageToSend = templateService.renderEmbedTemplate(PROFANITY_REPORT_TEMPLATE_KEY, reportModel, serverId);
+ List> messageFutures = postTargetService
+ .sendEmbedInPostTarget(messageToSend, ProfanityFilterPostTarget.PROFANITY_FILTER_QUEUE, serverId);
+ Long profanityRegexId = foundProfanityRegex.getId();
+ return FutureUtils.toSingleFutureGeneric(messageFutures).thenCompose(aVoid -> {
+ Message createdMessage = messageFutures.get(0).join();
+ return self.afterReportCreation(message, serverId, profanityRegexId, createdMessage);
+ });
+ }
+
+ @Transactional
+ public CompletableFuture afterReportCreation(Message message, Long serverId, Long profanityRegexId, Message createdMessage) {
+ if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, serverId, ProfanityFilterMode.PROFANITY_VOTE)) {
+ CompletableFuture firstReaction = reactionService.addReactionToMessageAsync(ProfanityFilterService.REPORT_AGREE_EMOTE, serverId, createdMessage);
+ CompletableFuture secondReaction = reactionService.addReactionToMessageAsync(ProfanityFilterService.REPORT_DISAGREE_EMOTE, serverId, createdMessage);
+ return CompletableFuture.allOf(firstReaction, secondReaction).thenAccept(aVoid1 -> {
+ log.debug("Reaction added to message {} for a profanity report.", message.getId());
+ self.persistProfanityReport(message, createdMessage, profanityRegexId);
+ });
+ } else {
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ @Override
+ public boolean isMessageProfanityReport(Long messageId) {
+ return profanityUseManagementService.getProfanityUseViaReportMessageId(messageId).isPresent();
+ }
+
+ @Override
+ public void verifyProfanityUse(ProfanityUse profanityUse, VoteResult result) {
+ switch(result) {
+ case DISAGREEMENT:
+ profanityUse.setConfirmed(false);
+ metricService.incrementCounter(PROFANITIES_DISAGREEMENT);
+ break;
+ case AGREEMENT:
+ profanityUse.setConfirmed(true);
+ metricService.incrementCounter(PROFANITIES_AGREEMENT);
+ deleteProfaneMessage(profanityUse);
+ break;
+ default: throw new IllegalArgumentException("Final vote result given. No mapping to action found.");
+ }
+ profanityUse.setVerified(true);
+ }
+
+ @Override
+ public Long getPositiveReportCountForUser(AUserInAServer aUserInAServer) {
+ ProfanityUserInAServer profanityUserInAServer = profanityUserInServerManagementService.getProfanityUser(aUserInAServer);
+ return getPositiveReportCountForUser(profanityUserInAServer);
+ }
+
+ @Override
+ public Long getPositiveReportCountForUser(ProfanityUserInAServer aUserInAServer) {
+ return profanityUseManagementService.getPositiveReports(aUserInAServer);
+ }
+
+ @Override
+ public Long getFalseProfanityReportCountForUser(AUserInAServer aUserInAServer) {
+ ProfanityUserInAServer profanityUserInAServer = profanityUserInServerManagementService.getProfanityUser(aUserInAServer);
+ return getFalseProfanityReportCountForUser(profanityUserInAServer);
+ }
+
+ @Override
+ public Long getFalseProfanityReportCountForUser(ProfanityUserInAServer aUserInAServer) {
+ return profanityUseManagementService.getFalsePositiveReports(aUserInAServer);
+ }
+
+ @Override
+ public List getRecentPositiveReports(AUserInAServer aUserInAServer, int count) {
+ ProfanityUserInAServer profanityUserInAServer = profanityUserInServerManagementService.getProfanityUser(aUserInAServer);
+ return getRecentPositiveReports(profanityUserInAServer, count);
+ }
+
+ private void deleteProfaneMessage(ProfanityUse profanityUse) {
+ messageService.deleteMessageInChannelInServer(profanityUse.getServer().getId(), profanityUse.getProfaneChannel().getId(), profanityUse.getProfaneMessageId())
+ .exceptionally(throwable -> {
+ log.info("Failed to delete profane message ");
+ return null;
+ });
+ }
+
+ @Override
+ public List getRecentPositiveReports(ProfanityUserInAServer aUserInAServer, int count) {
+ return profanityUseManagementService.getMostRecentProfanityReports(aUserInAServer, count)
+ .stream()
+ .map(profanityUse -> ServerChannelMessage
+ .builder()
+ .messageId(profanityUse.getReportMessageId())
+ .channelId(profanityUse.getReportChannel().getId())
+ .serverId(profanityUse.getServer().getId()).build())
+ .collect(Collectors.toList());
+ }
+
+ @Transactional
+ public void persistProfanityReport(Message profaneMessage, Message reportMessage, Long profanityRegexId) {
+ ServerChannelMessage profaneMessageObj = ServerChannelMessage.fromMessage(profaneMessage);
+ ServerChannelMessage reportMessageObj = ServerChannelMessage.fromMessage(reportMessage);
+ ProfanityRegex profanityRegex = profanityRegexManagementService.getProfanityRegexViaId(profanityRegexId);
+ AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(profaneMessage.getMember());
+ ProfanityUserInAServer profaneUser = profanityUserInServerManagementService.getOrCreateProfanityUser(aUserInAServer);
+ profanityUseManagementService.createProfanityUse(profaneMessageObj, reportMessageObj, profaneUser, profanityRegex.getGroup());
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ metricService.registerCounter(PROFANITIES_AGREEMENT, "Amount of profanity votes resulting in agreement");
+ metricService.registerCounter(PROFANITIES_DISAGREEMENT, "Amount of profanity votes resulting in disagreement");
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementServiceBean.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementServiceBean.java
new file mode 100644
index 000000000..160e232c1
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementServiceBean.java
@@ -0,0 +1,63 @@
+package dev.sheldan.abstracto.profanityfilter.service.management;
+
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import dev.sheldan.abstracto.core.models.database.AChannel;
+import dev.sheldan.abstracto.core.models.database.ProfanityGroup;
+import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import dev.sheldan.abstracto.profanityfilter.repository.ProfanityUseRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Optional;
+
+@Component
+public class ProfanityUseManagementServiceBean implements ProfanityUseManagementService {
+
+ @Autowired
+ private ProfanityUseRepository profanityUseRepository;
+
+ @Autowired
+ private ChannelManagementService channelManagementService;
+
+ @Override
+ public ProfanityUse createProfanityUse(ServerChannelMessage profaneMessage, ServerChannelMessage reportMessage, ProfanityUserInAServer reportedUser, ProfanityGroup usedProfanityGroup) {
+ AChannel profaneChannel = channelManagementService.loadChannel(profaneMessage.getChannelId());
+ AChannel reportChannel = channelManagementService.loadChannel(reportMessage.getChannelId());
+ ProfanityUse profanityUse = ProfanityUse
+ .builder()
+ .profanityUser(reportedUser)
+ .profanityGroup(usedProfanityGroup)
+ .profaneMessageId(profaneMessage.getMessageId())
+ .profaneChannel(profaneChannel)
+ .reportMessageId(reportMessage.getMessageId())
+ .reportChannel(reportChannel)
+ .verified(false)
+ .server(reportedUser.getServer())
+ .build();
+ return profanityUseRepository.save(profanityUse);
+ }
+
+ @Override
+ public Optional getProfanityUseViaReportMessageId(Long messageId) {
+ return profanityUseRepository.findById(messageId);
+ }
+
+ @Override
+ public Long getPositiveReports(ProfanityUserInAServer profanityUserInAServer) {
+ return profanityUseRepository.countByProfanityUserAndVerifiedTrueAndConfirmedTrue(profanityUserInAServer);
+ }
+
+ @Override
+ public Long getFalsePositiveReports(ProfanityUserInAServer profanityUserInAServer) {
+ return profanityUseRepository.countByProfanityUserAndVerifiedTrueAndConfirmedFalse(profanityUserInAServer);
+ }
+
+ @Override
+ public List getMostRecentProfanityReports(ProfanityUserInAServer profanityUserInAServer, int count) {
+ return profanityUseRepository.findAllByProfanityUserAndConfirmedTrueOrderByCreatedDesc(profanityUserInAServer, PageRequest.of(0, count));
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementServiceBean.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementServiceBean.java
new file mode 100644
index 000000000..27dd16e28
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementServiceBean.java
@@ -0,0 +1,57 @@
+package dev.sheldan.abstracto.profanityfilter.service.management;
+
+import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import dev.sheldan.abstracto.profanityfilter.repository.ProfanityUserInServerRepository;
+import net.dv8tion.jda.api.entities.Member;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@Component
+public class ProfanityUserInServerManagementServiceBean implements ProfanityUserInServerManagementService {
+
+ @Autowired
+ private ProfanityUserInServerRepository repository;
+
+ @Autowired
+ private UserInServerManagementService userInServerManagementService;
+
+ @Override
+ public Optional getProfanityUserOptional(Member member) {
+ AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
+ return getProfanityUserOptional(userInAServer);
+ }
+
+ @Override
+ public Optional getProfanityUserOptional(AUserInAServer aUserInAServer) {
+ return repository.findById(aUserInAServer.getUserInServerId());
+ }
+
+ @Override
+ public ProfanityUserInAServer getProfanityUser(AUserInAServer aUserInAServer) {
+ return getProfanityUserOptional(aUserInAServer).orElseThrow(() -> new AbstractoRunTimeException("Profanity user in server not found."));
+ }
+
+ @Override
+ public ProfanityUserInAServer createProfanityUser(AUserInAServer aUserInAServer) {
+ ProfanityUserInAServer profanityUserInAServer = ProfanityUserInAServer
+ .builder()
+ .user(aUserInAServer)
+ .id(aUserInAServer.getUserInServerId())
+ .server(aUserInAServer.getServerReference())
+ .build();
+ return repository.save(profanityUserInAServer);
+ }
+
+ @Override
+ public ProfanityUserInAServer getOrCreateProfanityUser(AUserInAServer aUserInAServer) {
+ Optional profanityUserOptional = getProfanityUserOptional(aUserInAServer);
+ return profanityUserOptional.orElseGet(() -> createProfanityUser(aUserInAServer));
+ }
+
+
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/collection.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/collection.xml
new file mode 100644
index 000000000..30263db2b
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/collection.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/command.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/command.xml
new file mode 100644
index 000000000..5c46427a5
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/command.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/data.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/data.xml
new file mode 100644
index 000000000..ae553933b
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/data.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/default_emote.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/default_emote.xml
new file mode 100644
index 000000000..855a522a6
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/default_emote.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/effect_types.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/effect_types.xml
new file mode 100644
index 000000000..a0449ad91
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/effect_types.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/feature.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/feature.xml
new file mode 100644
index 000000000..1f514506f
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/feature.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/module.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/module.xml
new file mode 100644
index 000000000..15f7d3e9e
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/seedData/module.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ SELECT COUNT(*) FROM module WHERE name='moderation';
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_use.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_use.xml
new file mode 100644
index 000000000..057e477c5
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_use.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DROP TRIGGER IF EXISTS profanity_use_update_trigger ON profanity_use;
+ CREATE TRIGGER profanity_use_update_trigger BEFORE UPDATE ON profanity_use FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
+
+
+ DROP TRIGGER IF EXISTS profanity_use_insert_trigger ON profanity_use;
+ CREATE TRIGGER profanity_use_insert_trigger BEFORE INSERT ON profanity_use FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_user_in_server.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_user_in_server.xml
new file mode 100644
index 000000000..c48229c4a
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/profanity_user_in_server.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DROP TRIGGER IF EXISTS profanity_user_in_server_update_trigger ON profanity_user_in_server;
+ CREATE TRIGGER profanity_user_in_server_update_trigger BEFORE UPDATE ON profanity_user_in_server FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
+
+
+ DROP TRIGGER IF EXISTS profanity_user_in_server_insert_trigger ON profanity_user_in_server;
+ CREATE TRIGGER profanity_user_in_server_insert_trigger BEFORE INSERT ON profanity_user_in_server FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/tables.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/tables.xml
new file mode 100644
index 000000000..87327c098
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/1.2.12/tables/tables.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/dbchangelog.xsd b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/dbchangelog.xsd
new file mode 100644
index 000000000..83483a5ec
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/dbchangelog.xsd
@@ -0,0 +1,1386 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extension to standard XSD boolean type to allow ${} parameters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extension to standard XSD integer type to allow ${} parameters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onChangeLogPreconditionOnSqlOutput determines what should
+ happen when evaluating this precondition in updateSQL mode. TEST: Run
+ precondition, FAIL: Fail precondition, IGNORE: Skip precondition check
+ [DEFAULT]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Used with valueClobFile to specify file encoding explicitly.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true for a cycling sequence, false for a non-cycling sequence.
+ Default is false.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/profanityFilter-changeLog.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/profanityFilter-changeLog.xml
new file mode 100644
index 000000000..89219968b
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/migrations/profanityFilter-changeLog.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/profanityFilter-config.properties b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/profanityFilter-config.properties
new file mode 100644
index 000000000..2aa4427a6
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-impl/src/main/resources/profanityFilter-config.properties
@@ -0,0 +1,27 @@
+abstracto.featureFlags.profanityFilter.featureName=profanityFilter
+abstracto.featureFlags.profanityFilter.enabled=false
+
+abstracto.postTargets.profanityQueue.name=profanityQueue
+
+abstracto.featureModes.profanityVote.featureName=profanityFilter
+abstracto.featureModes.profanityVote.mode=profanityVote
+abstracto.featureModes.profanityVote.enabled=true
+
+abstracto.featureModes.autoDeleteProfanities.featureName=profanityFilter
+abstracto.featureModes.autoDeleteProfanities.mode=autoDeleteProfanities
+abstracto.featureModes.autoDeleteProfanities.enabled=false
+
+abstracto.featureModes.profanityReport.featureName=profanityFilter
+abstracto.featureModes.profanityReport.mode=profanityReport
+abstracto.featureModes.profanityReport.enabled=true
+
+abstracto.featureModes.trackProfanities.featureName=profanityFilter
+abstracto.featureModes.trackProfanities.mode=trackProfanities
+abstracto.featureModes.trackProfanities.enabled=true
+
+abstracto.featureModes.autoDeleteAfterVote.featureName=profanityFilter
+abstracto.featureModes.autoDeleteAfterVote.mode=autoDeleteAfterVote
+abstracto.featureModes.autoDeleteAfterVote.enabled=true
+
+abstracto.systemConfigs.profanityVotes.name=profanityVotes
+abstracto.systemConfigs.profanityVotes.longValue=5
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/pom.xml b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/pom.xml
new file mode 100644
index 000000000..8c550bdb5
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ dev.sheldan.abstracto.modules
+ profanity-filter
+ 1.2.12-SNAPSHOT
+
+ 4.0.0
+
+ profanity-filter-int
+
+
+ 8
+ 8
+
+
+
+
+ dev.sheldan.abstracto.core
+ core-int
+ ${project.version}
+
+
+
+
\ No newline at end of file
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureConfig.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureConfig.java
new file mode 100644
index 000000000..25ab7a5f1
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureConfig.java
@@ -0,0 +1,46 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import dev.sheldan.abstracto.core.config.FeatureConfig;
+import dev.sheldan.abstracto.core.config.FeatureDefinition;
+import dev.sheldan.abstracto.core.config.FeatureMode;
+import dev.sheldan.abstracto.core.config.PostTargetEnum;
+import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class ProfanityFilterFeatureConfig implements FeatureConfig {
+ @Override
+ public FeatureDefinition getFeature() {
+ return ProfanityFilterFeatureDefinition.PROFANITY_FILTER;
+ }
+
+ @Override
+ public List getRequiredPostTargets() {
+ return Arrays.asList(ProfanityFilterPostTarget.PROFANITY_FILTER_QUEUE);
+ }
+
+ @Override
+ public List getAvailableModes() {
+ return Arrays.asList(
+ ProfanityFilterMode.PROFANITY_VOTE,
+ ProfanityFilterMode.AUTO_DELETE_PROFANITIES,
+ ProfanityFilterMode.TRACK_PROFANITIES,
+ ProfanityFilterMode.AUTO_DELETE_AFTER_VOTE
+ );
+ }
+
+ @Override
+ public List getRequiredSystemConfigKeys() {
+ return Arrays.asList(ProfanityFilterService.PROFANITY_VOTES_CONFIG_KEY);
+ }
+
+ @Override
+ public List getRequiredEmotes() {
+ return Arrays.asList(ProfanityFilterService.REPORT_AGREE_EMOTE, ProfanityFilterService.REPORT_DISAGREE_EMOTE);
+ }
+
+
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureDefinition.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureDefinition.java
new file mode 100644
index 000000000..ff4a718ba
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterFeatureDefinition.java
@@ -0,0 +1,15 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import dev.sheldan.abstracto.core.config.FeatureDefinition;
+import lombok.Getter;
+
+@Getter
+public enum ProfanityFilterFeatureDefinition implements FeatureDefinition {
+ PROFANITY_FILTER("profanityFilter");
+
+ private final String key;
+
+ ProfanityFilterFeatureDefinition(String key) {
+ this.key = key;
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterMode.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterMode.java
new file mode 100644
index 000000000..8feb16cf1
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterMode.java
@@ -0,0 +1,20 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import dev.sheldan.abstracto.core.config.FeatureMode;
+import lombok.Getter;
+
+@Getter
+public enum ProfanityFilterMode implements FeatureMode {
+ AUTO_DELETE_PROFANITIES("autoDeleteProfanities"),
+ PROFANITY_VOTE("profanityVote"),
+ PROFANITY_REPORT("profanityReport"),
+ AUTO_DELETE_AFTER_VOTE("autoDeleteAfterVote"),
+ TRACK_PROFANITIES("trackProfanities");
+
+ private final String key;
+
+ ProfanityFilterMode(String key) {
+ this.key = key;
+ }
+
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterModerationModuleDefinition.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterModerationModuleDefinition.java
new file mode 100644
index 000000000..380cac35d
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterModerationModuleDefinition.java
@@ -0,0 +1,21 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import dev.sheldan.abstracto.core.command.config.ModuleDefinition;
+import dev.sheldan.abstracto.core.command.config.ModuleInfo;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ProfanityFilterModerationModuleDefinition implements ModuleDefinition {
+
+ public static final String MODERATION = "moderation";
+
+ @Override
+ public ModuleInfo getInfo() {
+ return ModuleInfo.builder().name(MODERATION).templated(true).build();
+ }
+
+ @Override
+ public String getParentModule() {
+ return "default";
+ }
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterPostTarget.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterPostTarget.java
new file mode 100644
index 000000000..665e4f687
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/config/ProfanityFilterPostTarget.java
@@ -0,0 +1,16 @@
+package dev.sheldan.abstracto.profanityfilter.config;
+
+import dev.sheldan.abstracto.core.config.PostTargetEnum;
+import lombok.Getter;
+
+@Getter
+public enum ProfanityFilterPostTarget implements PostTargetEnum {
+ PROFANITY_FILTER_QUEUE("profanityQueue");
+
+ private String key;
+
+ ProfanityFilterPostTarget(String key) {
+ this.key = key;
+ }
+}
+
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUse.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUse.java
new file mode 100644
index 000000000..b34ac6d73
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUse.java
@@ -0,0 +1,59 @@
+package dev.sheldan.abstracto.profanityfilter.model.database;
+
+import dev.sheldan.abstracto.core.models.database.AChannel;
+import dev.sheldan.abstracto.core.models.database.AServer;
+import dev.sheldan.abstracto.core.models.database.ProfanityGroup;
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Builder
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "profanity_use")
+@Getter
+@Setter
+@EqualsAndHashCode
+public class ProfanityUse {
+
+ @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
+ @JoinColumn(name = "profanity_group_id", referencedColumnName = "id", nullable = false)
+ private ProfanityGroup profanityGroup;
+
+ @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
+ @JoinColumn(name = "profanity_user_in_server_id", referencedColumnName = "id", nullable = false)
+ private ProfanityUserInAServer profanityUser;
+
+ @Column(name = "report_message_id")
+ @Id
+ private Long reportMessageId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "report_channel_id", nullable = false)
+ private AChannel reportChannel;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "server_id", nullable = false)
+ private AServer server;
+
+ @Column(name = "profane_message_id")
+ private Long profaneMessageId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "profane_channel_id", nullable = false)
+ private AChannel profaneChannel;
+
+ @Column(name = "confirmed")
+ private Boolean confirmed;
+
+ @Column(name = "verified")
+ private Boolean verified;
+
+ @Column(name = "created")
+ private Instant created;
+
+ @Column(name = "updated")
+ private Instant updated;
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUserInAServer.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUserInAServer.java
new file mode 100644
index 000000000..c259da6dd
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/database/ProfanityUserInAServer.java
@@ -0,0 +1,43 @@
+package dev.sheldan.abstracto.profanityfilter.model.database;
+
+import dev.sheldan.abstracto.core.models.database.AServer;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+import java.util.List;
+
+@Builder
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "profanity_user_in_server")
+@Getter
+@Setter
+@EqualsAndHashCode
+public class ProfanityUserInAServer {
+ @Id
+ @Column(name = "id")
+ private Long id;
+
+ /**
+ * The {@link AUserInAServer user} which is represented by this object
+ */
+ @OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
+ @PrimaryKeyJoinColumn
+ private AUserInAServer user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "server_id", nullable = false)
+ private AServer server;
+
+ @Column(name = "created")
+ private Instant created;
+
+ @Column(name = "updated")
+ private Instant updated;
+
+ @OneToMany(mappedBy = "profanityUser", fetch = FetchType.LAZY)
+ private List usedProfanities;
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanitiesModel.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanitiesModel.java
new file mode 100644
index 000000000..29d73978a
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanitiesModel.java
@@ -0,0 +1,19 @@
+package dev.sheldan.abstracto.profanityfilter.model.template;
+
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+import net.dv8tion.jda.api.entities.Member;
+
+import java.util.List;
+
+@Getter
+@Setter
+@Builder
+public class ProfanitiesModel {
+ private Member member;
+ private Long falsePositives;
+ private Long truePositives;
+ private List recentPositiveReports;
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanityReportModel.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanityReportModel.java
new file mode 100644
index 000000000..de78c9211
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/model/template/ProfanityReportModel.java
@@ -0,0 +1,15 @@
+package dev.sheldan.abstracto.profanityfilter.model.template;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+import net.dv8tion.jda.api.entities.Message;
+
+@Getter
+@Setter
+@Builder
+public class ProfanityReportModel {
+ private String profanityGroupKey;
+ private String profanityRegexName;
+ private Message profaneMessage;
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterService.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterService.java
new file mode 100644
index 000000000..3bf56a3a6
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/ProfanityFilterService.java
@@ -0,0 +1,35 @@
+package dev.sheldan.abstracto.profanityfilter.service;
+
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import net.dv8tion.jda.api.entities.Message;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+public interface ProfanityFilterService {
+ String REPORT_AGREE_EMOTE = "profanityFilterAgreeEmote";
+ String PROFANITY_VOTES_CONFIG_KEY = "profanityVotes";
+ String REPORT_DISAGREE_EMOTE = "profanityFilterDisagreeEmote";
+ String PROFANITY_FILTER_EFFECT_KEY = "profanityFilter";
+
+ enum VoteResult {
+ AGREEMENT, DISAGREEMENT, BELOW_THRESHOLD;
+ public static boolean isFinal(VoteResult result) {
+ return result.equals(AGREEMENT) || result.equals(DISAGREEMENT);
+ }
+ }
+
+ CompletableFuture createProfanityReport(Message message, ProfanityRegex profanityRegex);
+ boolean isMessageProfanityReport(Long messageId);
+ void verifyProfanityUse(ProfanityUse profanityUse, VoteResult result);
+ Long getPositiveReportCountForUser(AUserInAServer aUserInAServer);
+ Long getPositiveReportCountForUser(ProfanityUserInAServer aUserInAServer);
+ Long getFalseProfanityReportCountForUser(AUserInAServer aUserInAServer);
+ Long getFalseProfanityReportCountForUser(ProfanityUserInAServer aUserInAServer);
+ List getRecentPositiveReports(AUserInAServer aUserInAServer, int count);
+ List getRecentPositiveReports(ProfanityUserInAServer aUserInAServer, int count);
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementService.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementService.java
new file mode 100644
index 000000000..f9916fd01
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUseManagementService.java
@@ -0,0 +1,17 @@
+package dev.sheldan.abstracto.profanityfilter.service.management;
+
+import dev.sheldan.abstracto.core.models.ServerChannelMessage;
+import dev.sheldan.abstracto.core.models.database.ProfanityGroup;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUse;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ProfanityUseManagementService {
+ ProfanityUse createProfanityUse(ServerChannelMessage profaneMessage, ServerChannelMessage reportMessage, ProfanityUserInAServer reportedUser, ProfanityGroup usedProfanityGroup);
+ Optional getProfanityUseViaReportMessageId(Long messageId);
+ Long getPositiveReports(ProfanityUserInAServer profanityUserInAServer);
+ Long getFalsePositiveReports(ProfanityUserInAServer profanityUserInAServer);
+ List getMostRecentProfanityReports(ProfanityUserInAServer profanityUserInAServer, int count);
+}
diff --git a/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementService.java b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementService.java
new file mode 100644
index 000000000..0cd5ee4de
--- /dev/null
+++ b/abstracto-application/abstracto-modules/profanity-filter/profanity-filter-int/src/main/java/dev/sheldan/abstracto/profanityfilter/service/management/ProfanityUserInServerManagementService.java
@@ -0,0 +1,15 @@
+package dev.sheldan.abstracto.profanityfilter.service.management;
+
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.profanityfilter.model.database.ProfanityUserInAServer;
+import net.dv8tion.jda.api.entities.Member;
+
+import java.util.Optional;
+
+public interface ProfanityUserInServerManagementService {
+ Optional getProfanityUserOptional(Member member);
+ Optional getProfanityUserOptional(AUserInAServer aUserInAServer);
+ ProfanityUserInAServer getProfanityUser(AUserInAServer aUserInAServer);
+ ProfanityUserInAServer createProfanityUser(AUserInAServer aUserInAServer);
+ ProfanityUserInAServer getOrCreateProfanityUser(AUserInAServer aUserInAServer);
+}
diff --git a/abstracto-application/abstracto-modules/remind/remind-impl/pom.xml b/abstracto-application/abstracto-modules/remind/remind-impl/pom.xml
index 2b6a04dd4..5bceb4ffe 100644
--- a/abstracto-application/abstracto-modules/remind/remind-impl/pom.xml
+++ b/abstracto-application/abstracto-modules/remind/remind-impl/pom.xml
@@ -55,7 +55,6 @@
${project.version}
-
dev.sheldan.abstracto.core
core-int
@@ -63,8 +62,6 @@
test-jar
test
-
-
\ No newline at end of file
diff --git a/abstracto-application/bundle/pom.xml b/abstracto-application/bundle/pom.xml
index 298f0da06..4d07155a0 100644
--- a/abstracto-application/bundle/pom.xml
+++ b/abstracto-application/bundle/pom.xml
@@ -206,7 +206,7 @@
dev.sheldan.abstracto.modules
- loggingg-int
+ logging-int
${project.version}
@@ -216,6 +216,18 @@
${project.version}
+
+ dev.sheldan.abstracto.modules
+ profanity-filter-int
+ ${project.version}
+
+
+
+ dev.sheldan.abstracto.modules
+ profanity-filter-impl
+ ${project.version}
+
+
dev.sheldan.abstracto.modules
webservices-int
diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/ProfanityServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/ProfanityServiceBean.java
index a313b94c4..0b194997b 100644
--- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/ProfanityServiceBean.java
+++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/ProfanityServiceBean.java
@@ -68,17 +68,22 @@ public class ProfanityServiceBean implements ProfanityService {
@Override
public boolean containsProfanity(String input, Long serverId) {
+ return getProfanityRegex(input, serverId).isPresent();
+ }
+
+ @Override
+ public Optional getProfanityRegex(String input, Long serverId) {
if(regex.containsKey(serverId)) {
List regexes = regex.get(serverId);
log.debug("Checking existence of {} regexes for server {}.", regexes.size(), serverId);
for (PatternReplacement pattern: regexes) {
Matcher matcher = pattern.getPattern().matcher(input);
- if(matcher.matches()) {
- return true;
+ if(matcher.find()) {
+ return profanityRegexManagementService.getProfanityRegexViaIdOptional(pattern.profanityRegexId);
}
}
}
- return false;
+ return Optional.empty();
}
@Override
@@ -132,18 +137,28 @@ public class ProfanityServiceBean implements ProfanityService {
regex = new HashMap<>();
List allGroups = profanityGroupManagementService.getAllGroups();
allGroups.forEach(profanityGroup -> profanityGroup.getProfanities().forEach(profanityRegex -> {
- Pattern pattern = Pattern.compile(profanityRegex.getRegex());
- List newPatterns = new ArrayList<>();
Long serverId = profanityGroup.getServer().getId();
- if(regex.containsKey(serverId)) {
- regex.get(serverId).add(PatternReplacement.builder().pattern(pattern).replacement(profanityRegex.getReplacement()).build());
- } else {
- newPatterns.add(PatternReplacement.builder().pattern(pattern).replacement(profanityRegex.getReplacement()).build());
- regex.put(serverId, newPatterns);
- }
+ loadProfanityRegex(profanityRegex, serverId);
}));
}
+ private void loadProfanityRegex(ProfanityRegex profanityRegex, Long serverId) {
+ Pattern pattern = Pattern.compile(profanityRegex.getRegex());
+ List newPatterns = new ArrayList<>();
+ PatternReplacement patternReplacement = PatternReplacement
+ .builder()
+ .pattern(pattern)
+ .replacement(profanityRegex.getReplacement())
+ .profanityRegexId(profanityRegex.getId())
+ .build();
+ if (regex.containsKey(serverId)) {
+ regex.get(serverId).add(patternReplacement);
+ } else {
+ newPatterns.add(patternReplacement);
+ regex.put(serverId, newPatterns);
+ }
+ }
+
@Override
public void reloadRegex(Long serverId) {
log.info("Reloading regex for server {}.", serverId);
@@ -152,22 +167,16 @@ public class ProfanityServiceBean implements ProfanityService {
}
regex.remove(serverId);
List allGroups = profanityGroupManagementService.getAllForServer(serverId);
- allGroups.forEach(profanityGroup -> profanityGroup.getProfanities().forEach(profanityRegex -> {
- Pattern pattern = Pattern.compile(profanityRegex.getRegex());
- List newPatterns = new ArrayList<>();
- if(regex.containsKey(serverId)) {
- regex.get(serverId).add(PatternReplacement.builder().pattern(pattern).replacement(profanityRegex.getReplacement()).build());
- } else {
- newPatterns.add(PatternReplacement.builder().pattern(pattern).replacement(profanityRegex.getReplacement()).build());
- regex.put(serverId, newPatterns);
- }
- }));
+ allGroups
+ .forEach(profanityGroup -> profanityGroup.getProfanities()
+ .forEach(profanityRegex -> loadProfanityRegex(profanityRegex, serverId)));
}
@Getter
@Builder
private static class PatternReplacement {
+ private final Long profanityRegexId;
private final Pattern pattern;
private final String replacement;
}
diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementServiceBean.java
index fd473b6a2..a5489e621 100644
--- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementServiceBean.java
+++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementServiceBean.java
@@ -44,6 +44,16 @@ public class ProfanityGroupManagementServiceBean implements ProfanityGroupManage
return getProfanityGroupOptional(server, name).isPresent();
}
+ @Override
+ public Optional getProfanityGroupByIdOptional(Long profanityGroupId) {
+ return repository.findById(profanityGroupId);
+ }
+
+ @Override
+ public ProfanityGroup getProfanityGroupById(Long profanityGroupId) {
+ return getProfanityGroupByIdOptional(profanityGroupId).orElseThrow(ProfanityGroupNotFoundException::new);
+ }
+
@Override
public Optional getProfanityGroupOptional(AServer server, String name) {
return repository.findByServerAndGroupNameIgnoreCase(server, name);
diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementServiceBean.java
index 6dad5cf1f..6b855f33a 100644
--- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementServiceBean.java
+++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementServiceBean.java
@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.core.service.management;
+import dev.sheldan.abstracto.core.exception.ProfanityRegexNotFoundException;
import dev.sheldan.abstracto.core.models.database.ProfanityGroup;
import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
import dev.sheldan.abstracto.core.repository.ProfanityRegexRepository;
@@ -54,4 +55,14 @@ public class ProfanityRegexManagementServiceBean implements ProfanityRegexManag
public Optional getProfanityRegexOptional(ProfanityGroup profanityGroup, String name) {
return repository.findByGroupAndRegexNameIgnoreCase(profanityGroup, name);
}
+
+ @Override
+ public Optional getProfanityRegexViaIdOptional(Long profanityRegexId) {
+ return repository.findById(profanityRegexId);
+ }
+
+ @Override
+ public ProfanityRegex getProfanityRegexViaId(Long profanityRegexId) {
+ return getProfanityRegexViaIdOptional(profanityRegexId).orElseThrow(ProfanityRegexNotFoundException::new);
+ }
}
diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/ServerChannelMessage.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/ServerChannelMessage.java
index a05959726..6a5863217 100644
--- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/ServerChannelMessage.java
+++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/ServerChannelMessage.java
@@ -4,6 +4,7 @@ import dev.sheldan.abstracto.core.utils.MessageUtils;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
+import net.dv8tion.jda.api.entities.Message;
@Getter
@Setter
@@ -16,4 +17,13 @@ public class ServerChannelMessage {
public String getJumpUrl() {
return MessageUtils.buildMessageUrl(serverId, channelId, messageId);
}
+
+ public static ServerChannelMessage fromMessage(Message message) {
+ return ServerChannelMessage
+ .builder()
+ .serverId(message.getGuild().getIdLong())
+ .channelId(message.getChannel().getIdLong())
+ .messageId(message.getIdLong())
+ .build();
+ }
}
diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/ProfanityService.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/ProfanityService.java
index 32b1e975b..7e6da03a6 100644
--- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/ProfanityService.java
+++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/ProfanityService.java
@@ -3,11 +3,14 @@ package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.database.ProfanityGroup;
import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
+import java.util.Optional;
+
public interface ProfanityService {
String replaceProfanities(String input, Long serverId);
String replaceProfanities(String input, Long serverId, String replacement);
String replaceProfanitiesWithDefault(String input, Long serverId, String replacement);
boolean containsProfanity(String input, Long serverId);
+ Optional getProfanityRegex(String input, Long serverId);
ProfanityGroup createProfanityGroup(Long serverId, String profanityGroupName);
void deleteProfanityGroup(Long serverId, String profanityGroupName);
void deleteProfanityRegex(Long serverId, String profanityGroupName, String profanityRegexName);
diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementService.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementService.java
index 3e7ed3a72..d61ca8bb6 100644
--- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementService.java
+++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityGroupManagementService.java
@@ -11,6 +11,8 @@ public interface ProfanityGroupManagementService {
List getAllForServer(Long serverId);
ProfanityGroup createProfanityGroup(AServer server, String name);
boolean doesProfanityGroupExist(AServer server, String name);
+ Optional getProfanityGroupByIdOptional(Long profanityGroupId);
+ ProfanityGroup getProfanityGroupById(Long profanityGroupId);
Optional getProfanityGroupOptional(AServer server, String name);
ProfanityGroup getProfanityGroup(AServer server, String name);
void deleteProfanityGroup(ProfanityGroup profanityGroup);
diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementService.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementService.java
index 7d5144f60..da3aae484 100644
--- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementService.java
+++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/service/management/ProfanityRegexManagementService.java
@@ -12,4 +12,6 @@ public interface ProfanityRegexManagementService {
void deleteProfanityRegex(ProfanityGroup group, String profanityName);
boolean doesProfanityRegexExist(ProfanityGroup profanityGroup, String name);
Optional getProfanityRegexOptional(ProfanityGroup profanityGroup, String name);
+ Optional getProfanityRegexViaIdOptional(Long profanityRegexId);
+ ProfanityRegex getProfanityRegexViaId(Long profanityRegexId);
}
diff --git a/abstracto-application/documentation/src/main/docs/asciidoc/modules/core.adoc b/abstracto-application/documentation/src/main/docs/asciidoc/modules/core.adoc
index cf63a6db5..da1d5e814 100644
--- a/abstracto-application/documentation/src/main/docs/asciidoc/modules/core.adoc
+++ b/abstracto-application/documentation/src/main/docs/asciidoc/modules/core.adoc
@@ -13,7 +13,7 @@ Help::
This information includes a description and the available commands of this module. If the provided parameter matches a command name, information about this command is displayed.
The module matching takes precedence over command matching.
This information includes the a short description, a more detailed description, aliases (if any), parameters (if any), which roles are allowed to execute the command,
-or if it is not restricted and which roles are immune against the command.
+or if it is not restricted and which effects a command has.
Changing the system configuration::
* Usage `setConfig `
* Description: Changes the value of this configuration identified by `key` to `value`. Some of these configurations have separate commands, but this works in general.
diff --git a/abstracto-application/documentation/src/main/docs/asciidoc/modules/moderation.adoc b/abstracto-application/documentation/src/main/docs/asciidoc/modules/moderation.adoc
index 58c2842b4..c1b7a6b17 100644
--- a/abstracto-application/documentation/src/main/docs/asciidoc/modules/moderation.adoc
+++ b/abstracto-application/documentation/src/main/docs/asciidoc/modules/moderation.adoc
@@ -198,4 +198,30 @@ Showing the tracked filtered invites::
Remove all or individual invites from the tracked filtered invites::
* Usage: `removeTrackedInviteLinks [invite]`
* Description: Removes the stored statistic for the given `invite`. In case `invite` is not given, it will delete all tracked filtered invites from the server.
-* Mode Restriction: This command is only available when the feature mode `trackUses` is enabled.
\ No newline at end of file
+* Mode Restriction: This command is only available when the feature mode `trackUses` is enabled.
+
+=== Profanity filter
+
+Feature key `profanityFilter`
+
+This functionality provides the ability to automatically delete any detected profanities. These profanities are configured via the profanity groups and profanity regexes. Every group in these groups are active and every profanity regex will be evaluated and (depending on the feature mode) reported to be voted on.
+The uses of profanities can be tracked and a command is available to show the profanities for a user.
+
+==== Post targets
+`profanityQueue`:: target for reports to be voted on - if the feature mode `filterNotifications` is enabled.
+
+==== Feature modes
+`autoDeleteProfanities`:: if enabled, each detected profanity will be deleted immediately. Disabled by default.
+`profanityReport`:: if enabled, sends a notification to the `profanityQueue` post target to notify about a detected profanity. Enabled by default.
+`profanityVote`:: if enabled, sends a notification to the `profanityQueue` post target to notify about a detected profanity to be voted on. Requires feature mode `profanityReport` to be enabled. Enabled by default.
+`autoDeleteAfterVote`:: if enabled, after a profanity vote has reached the threshold (system config key `profanityVotes`), depending on the outcome, it will be deleted. Requires feature mode `profanityVote` to be enabled. Enabled by default.
+`trackProfanities`:: if enabled, the command `profanities` is available to show the profanities of a member. Requires feature mode `profanityVote` to be enabled. Enabled by default.
+
+==== Emotes
+* `profanityFilterAgreeEmote` reaction emote to indicate agreement about a reported profanity
+* `profanityFilterDisagreeEmote` reaction emote to indicate disagreement about a reported profanity
+
+==== Commands
+Show the profanities of a member::
+* Usage `profanities `
+* Description: Shows the true and false positive profanities of the given member. Also, if there any, shows the recent true positive reports.
\ No newline at end of file