[AB-84] adding profanity filter with different feature modes

react command: adding additional mapping for character r
This commit is contained in:
Sheldan
2021-05-16 23:44:50 +02:00
parent eca9e6ebf7
commit 04a7cfafd7
53 changed files with 2875 additions and 34 deletions

View File

@@ -119,7 +119,8 @@
"♌" "♌"
], ],
"r": [ "r": [
"🇷" "🇷",
"🌱"
], ],
"s": [ "s": [
"🇸", "🇸",

View File

@@ -57,7 +57,8 @@ public class ExperienceLevelServiceBean implements ExperienceLevelService {
@Override @Override
public Long calculateExperienceToNextLevel(Integer level, Long currentExperience) { 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; return nextLevel.getExperienceNeeded() - currentExperience;
} }

View File

@@ -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.ServerUser;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel; import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.*; 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.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList; 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.Invite;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -67,14 +65,14 @@ public class InviteLinkFilterListener implements AsyncMessageReceivedListener {
@Autowired @Autowired
private RoleImmunityService roleImmunityService; 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"; public static final String CONSEQUENCE = "consequence";
private static final CounterMetric MESSAGE_INVITE_FILTERED = private static final CounterMetric MESSAGE_INVITE_FILTERED =
CounterMetric CounterMetric
.builder() .builder()
.tagList(Arrays.asList(MetricTag.getTag(CONSEQUENCE, "filtered"))) .tagList(Arrays.asList(MetricTag.getTag(CONSEQUENCE, "filtered")))
.name(MODERATION_PURGE_METRIC) .name(INVITE_FILTER_METRIC)
.build(); .build();
public static final String INVITE_LINK_DELETED_NOTIFICATION_EMBED_TEMPLATE_KEY = "invite_link_deleted_notification"; public static final String INVITE_LINK_DELETED_NOTIFICATION_EMBED_TEMPLATE_KEY = "invite_link_deleted_notification";

View File

@@ -26,7 +26,7 @@
<module>webservices</module> <module>webservices</module>
<module>logging</module> <module>logging</module>
<module>invite-filter</module> <module>invite-filter</module>
<module>profanity-filter</module>
</modules> </modules>
</project> </project>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.2.12-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>profanity-filter</artifactId>
<packaging>pom</packaging>
<modules>
<module>profanity-filter-int</module>
<module>profanity-filter-impl</module>
</modules>
</project>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>profanity-filter</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.2.12-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>profanity-filter-impl</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/liquibase.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>profanity-filter-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,18 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>liquibase</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>.</outputDirectory>
<directory>${project.basedir}/src/main/resources/migrations</directory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -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<CommandResult> 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<ServerChannelMessage> 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<Parameter> 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<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ProfanityFilterMode.TRACK_PROFANITIES);
}
}

View File

@@ -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 {
}

View File

@@ -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<ProfanityRegex> 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");
}
}

View File

@@ -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<ProfanityUse> 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<CachedReactions> agreementReactionsOptional = emoteService.getReactionFromMessageByEmote(cachedMessage, agreementEmote);
Optional<CachedReactions> 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;
}
}

View File

@@ -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<ProfanityUse, Long> {
Long countByProfanityUserAndVerifiedTrueAndConfirmedTrue(ProfanityUserInAServer profanityUserInAServer);
Long countByProfanityUserAndVerifiedTrueAndConfirmedFalse(ProfanityUserInAServer profanityUserInAServer);
List<ProfanityUse> findAllByProfanityUserAndConfirmedTrueOrderByCreatedDesc(ProfanityUserInAServer profanityUserInAServer, Pageable pageable);
}

View File

@@ -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<ProfanityUserInAServer, Long> {
}

View File

@@ -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<Void> 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<CompletableFuture<Message>> 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<Void> afterReportCreation(Message message, Long serverId, Long profanityRegexId, Message createdMessage) {
if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, serverId, ProfanityFilterMode.PROFANITY_VOTE)) {
CompletableFuture<Void> firstReaction = reactionService.addReactionToMessageAsync(ProfanityFilterService.REPORT_AGREE_EMOTE, serverId, createdMessage);
CompletableFuture<Void> 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<ServerChannelMessage> 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<ServerChannelMessage> 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");
}
}

View File

@@ -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<ProfanityUse> 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<ProfanityUse> getMostRecentProfanityReports(ProfanityUserInAServer profanityUserInAServer, int count) {
return profanityUseRepository.findAllByProfanityUserAndConfirmedTrueOrderByCreatedDesc(profanityUserInAServer, PageRequest.of(0, count));
}
}

View File

@@ -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<ProfanityUserInAServer> getProfanityUserOptional(Member member) {
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
return getProfanityUserOptional(userInAServer);
}
@Override
public Optional<ProfanityUserInAServer> 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<ProfanityUserInAServer> profanityUserOptional = getProfanityUserOptional(aUserInAServer);
return profanityUserOptional.orElseGet(() -> createProfanityUser(aUserInAServer));
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,18 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<property name="profanityFilterFeature" value="(SELECT id FROM feature WHERE key = 'profanityFilter')"/>
<property name="moderationModule" value="(SELECT id FROM module WHERE name = 'moderation')"/>
<changeSet author="Sheldan" id="profanityFilter_command-commands">
<insert tableName="command">
<column name="name" value="profanities"/>
<column name="module_id" valueComputed="${moderationModule}"/>
<column name="feature_id" valueComputed="${profanityFilterFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="feature.xml" relativeToChangelogFile="true"/>
<include file="module.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
<include file="effect_types.xml" relativeToChangelogFile="true"/>
<include file="default_emote.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,19 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd">
<changeSet author="Sheldan" id="profanityFilter_emote-insert">
<insert tableName="default_emote">
<column name="emote_key" value="profanityFilterAgreeEmote"/>
<column name="name" value="⏫"/>
</insert>
<insert tableName="default_emote">
<column name="emote_key" value="profanityFilterDisagreeEmote"/>
<column name="name" value="⏬"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="profanityFilter_effect_type-insertion">
<insert tableName="effect_type">
<column name="effect_type_key" value="profanityFilter"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="profanityFilter_feature-insertion">
<insert tableName="feature">
<column name="key" value="profanityFilter"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,19 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="profanityFilter-moderation-module-insertion">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM module WHERE name='moderation';
</sqlCheck>
</preConditions>
<insert tableName="module">
<column name="name" value="moderation"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,76 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="profanity_use-table">
<createTable tableName="profanity_use">
<column name="report_message_id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_profanity_use"/>
</column>
<column name="profanity_group_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="profanity_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="report_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="profane_message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="profane_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="true"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="confirmed" type="BOOLEAN">
<constraints nullable="true"/>
</column>
<column name="verified" type="BOOLEAN">
<constraints nullable="true"/>
</column>
</createTable>
<createIndex indexName="idx_profanity_use_user_in_server" tableName="profanity_use">
<column name="profanity_user_in_server_id"/>
</createIndex>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="profanity_use" constraintName="fk_profanity_use_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="report_channel_id" baseTableName="profanity_use" constraintName="fk_profanity_use_report_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="profane_channel_id" baseTableName="profanity_use" constraintName="fk_profanity_use_profane_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="profanity_group_id" baseTableName="profanity_use" constraintName="fk_profanity_use_profanity_group"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="profanity_group" validate="true"/>
<addForeignKeyConstraint baseColumnNames="profanity_user_in_server_id" baseTableName="profanity_use" constraintName="fk_profanity_use_profanity_user"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="profanity_user_in_server" validate="true"/>
<sql>
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();
</sql>
<sql>
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();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,36 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="profanity_user_in_server-table">
<createTable tableName="profanity_user_in_server">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_profanity_user_in_server"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="true"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="profanity_user_in_server" constraintName="fk_profanity_user_in_server_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
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();
</sql>
<sql>
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();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="profanity_user_in_server.xml" relativeToChangelogFile="true"/>
<include file="profanity_use.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.2.12/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -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

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>profanity-filter</artifactId>
<version>1.2.12-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>profanity-filter-int</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(ProfanityFilterPostTarget.PROFANITY_FILTER_QUEUE);
}
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(
ProfanityFilterMode.PROFANITY_VOTE,
ProfanityFilterMode.AUTO_DELETE_PROFANITIES,
ProfanityFilterMode.TRACK_PROFANITIES,
ProfanityFilterMode.AUTO_DELETE_AFTER_VOTE
);
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(ProfanityFilterService.PROFANITY_VOTES_CONFIG_KEY);
}
@Override
public List<String> getRequiredEmotes() {
return Arrays.asList(ProfanityFilterService.REPORT_AGREE_EMOTE, ProfanityFilterService.REPORT_DISAGREE_EMOTE);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<ProfanityUse> usedProfanities;
}

View File

@@ -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<ServerChannelMessage> recentPositiveReports;
}

View File

@@ -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;
}

View File

@@ -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<Void> 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<ServerChannelMessage> getRecentPositiveReports(AUserInAServer aUserInAServer, int count);
List<ServerChannelMessage> getRecentPositiveReports(ProfanityUserInAServer aUserInAServer, int count);
}

View File

@@ -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<ProfanityUse> getProfanityUseViaReportMessageId(Long messageId);
Long getPositiveReports(ProfanityUserInAServer profanityUserInAServer);
Long getFalsePositiveReports(ProfanityUserInAServer profanityUserInAServer);
List<ProfanityUse> getMostRecentProfanityReports(ProfanityUserInAServer profanityUserInAServer, int count);
}

View File

@@ -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<ProfanityUserInAServer> getProfanityUserOptional(Member member);
Optional<ProfanityUserInAServer> getProfanityUserOptional(AUserInAServer aUserInAServer);
ProfanityUserInAServer getProfanityUser(AUserInAServer aUserInAServer);
ProfanityUserInAServer createProfanityUser(AUserInAServer aUserInAServer);
ProfanityUserInAServer getOrCreateProfanityUser(AUserInAServer aUserInAServer);
}

View File

@@ -55,7 +55,6 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.sheldan.abstracto.core</groupId> <groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId> <artifactId>core-int</artifactId>
@@ -63,8 +62,6 @@
<type>test-jar</type> <type>test-jar</type>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -206,7 +206,7 @@
<dependency> <dependency>
<groupId>dev.sheldan.abstracto.modules</groupId> <groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>loggingg-int</artifactId> <artifactId>logging-int</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
@@ -216,6 +216,18 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>profanity-filter-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>profanity-filter-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>dev.sheldan.abstracto.modules</groupId> <groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>webservices-int</artifactId> <artifactId>webservices-int</artifactId>

View File

@@ -68,17 +68,22 @@ public class ProfanityServiceBean implements ProfanityService {
@Override @Override
public boolean containsProfanity(String input, Long serverId) { public boolean containsProfanity(String input, Long serverId) {
return getProfanityRegex(input, serverId).isPresent();
}
@Override
public Optional<ProfanityRegex> getProfanityRegex(String input, Long serverId) {
if(regex.containsKey(serverId)) { if(regex.containsKey(serverId)) {
List<PatternReplacement> regexes = regex.get(serverId); List<PatternReplacement> regexes = regex.get(serverId);
log.debug("Checking existence of {} regexes for server {}.", regexes.size(), serverId); log.debug("Checking existence of {} regexes for server {}.", regexes.size(), serverId);
for (PatternReplacement pattern: regexes) { for (PatternReplacement pattern: regexes) {
Matcher matcher = pattern.getPattern().matcher(input); Matcher matcher = pattern.getPattern().matcher(input);
if(matcher.matches()) { if(matcher.find()) {
return true; return profanityRegexManagementService.getProfanityRegexViaIdOptional(pattern.profanityRegexId);
} }
} }
} }
return false; return Optional.empty();
} }
@Override @Override
@@ -132,18 +137,28 @@ public class ProfanityServiceBean implements ProfanityService {
regex = new HashMap<>(); regex = new HashMap<>();
List<ProfanityGroup> allGroups = profanityGroupManagementService.getAllGroups(); List<ProfanityGroup> allGroups = profanityGroupManagementService.getAllGroups();
allGroups.forEach(profanityGroup -> profanityGroup.getProfanities().forEach(profanityRegex -> { allGroups.forEach(profanityGroup -> profanityGroup.getProfanities().forEach(profanityRegex -> {
Pattern pattern = Pattern.compile(profanityRegex.getRegex());
List<PatternReplacement> newPatterns = new ArrayList<>();
Long serverId = profanityGroup.getServer().getId(); Long serverId = profanityGroup.getServer().getId();
if(regex.containsKey(serverId)) { loadProfanityRegex(profanityRegex, 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);
}
})); }));
} }
private void loadProfanityRegex(ProfanityRegex profanityRegex, Long serverId) {
Pattern pattern = Pattern.compile(profanityRegex.getRegex());
List<PatternReplacement> 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 @Override
public void reloadRegex(Long serverId) { public void reloadRegex(Long serverId) {
log.info("Reloading regex for server {}.", serverId); log.info("Reloading regex for server {}.", serverId);
@@ -152,22 +167,16 @@ public class ProfanityServiceBean implements ProfanityService {
} }
regex.remove(serverId); regex.remove(serverId);
List<ProfanityGroup> allGroups = profanityGroupManagementService.getAllForServer(serverId); List<ProfanityGroup> allGroups = profanityGroupManagementService.getAllForServer(serverId);
allGroups.forEach(profanityGroup -> profanityGroup.getProfanities().forEach(profanityRegex -> { allGroups
Pattern pattern = Pattern.compile(profanityRegex.getRegex()); .forEach(profanityGroup -> profanityGroup.getProfanities()
List<PatternReplacement> newPatterns = new ArrayList<>(); .forEach(profanityRegex -> loadProfanityRegex(profanityRegex, serverId)));
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);
}
}));
} }
@Getter @Getter
@Builder @Builder
private static class PatternReplacement { private static class PatternReplacement {
private final Long profanityRegexId;
private final Pattern pattern; private final Pattern pattern;
private final String replacement; private final String replacement;
} }

View File

@@ -44,6 +44,16 @@ public class ProfanityGroupManagementServiceBean implements ProfanityGroupManage
return getProfanityGroupOptional(server, name).isPresent(); return getProfanityGroupOptional(server, name).isPresent();
} }
@Override
public Optional<ProfanityGroup> getProfanityGroupByIdOptional(Long profanityGroupId) {
return repository.findById(profanityGroupId);
}
@Override
public ProfanityGroup getProfanityGroupById(Long profanityGroupId) {
return getProfanityGroupByIdOptional(profanityGroupId).orElseThrow(ProfanityGroupNotFoundException::new);
}
@Override @Override
public Optional<ProfanityGroup> getProfanityGroupOptional(AServer server, String name) { public Optional<ProfanityGroup> getProfanityGroupOptional(AServer server, String name) {
return repository.findByServerAndGroupNameIgnoreCase(server, name); return repository.findByServerAndGroupNameIgnoreCase(server, name);

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.core.service.management; 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.ProfanityGroup;
import dev.sheldan.abstracto.core.models.database.ProfanityRegex; import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
import dev.sheldan.abstracto.core.repository.ProfanityRegexRepository; import dev.sheldan.abstracto.core.repository.ProfanityRegexRepository;
@@ -54,4 +55,14 @@ public class ProfanityRegexManagementServiceBean implements ProfanityRegexManag
public Optional<ProfanityRegex> getProfanityRegexOptional(ProfanityGroup profanityGroup, String name) { public Optional<ProfanityRegex> getProfanityRegexOptional(ProfanityGroup profanityGroup, String name) {
return repository.findByGroupAndRegexNameIgnoreCase(profanityGroup, name); return repository.findByGroupAndRegexNameIgnoreCase(profanityGroup, name);
} }
@Override
public Optional<ProfanityRegex> getProfanityRegexViaIdOptional(Long profanityRegexId) {
return repository.findById(profanityRegexId);
}
@Override
public ProfanityRegex getProfanityRegexViaId(Long profanityRegexId) {
return getProfanityRegexViaIdOptional(profanityRegexId).orElseThrow(ProfanityRegexNotFoundException::new);
}
} }

View File

@@ -4,6 +4,7 @@ import dev.sheldan.abstracto.core.utils.MessageUtils;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.Message;
@Getter @Getter
@Setter @Setter
@@ -16,4 +17,13 @@ public class ServerChannelMessage {
public String getJumpUrl() { public String getJumpUrl() {
return MessageUtils.buildMessageUrl(serverId, channelId, messageId); 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();
}
} }

View File

@@ -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.ProfanityGroup;
import dev.sheldan.abstracto.core.models.database.ProfanityRegex; import dev.sheldan.abstracto.core.models.database.ProfanityRegex;
import java.util.Optional;
public interface ProfanityService { public interface ProfanityService {
String replaceProfanities(String input, Long serverId); String replaceProfanities(String input, Long serverId);
String replaceProfanities(String input, Long serverId, String replacement); String replaceProfanities(String input, Long serverId, String replacement);
String replaceProfanitiesWithDefault(String input, Long serverId, String replacement); String replaceProfanitiesWithDefault(String input, Long serverId, String replacement);
boolean containsProfanity(String input, Long serverId); boolean containsProfanity(String input, Long serverId);
Optional<ProfanityRegex> getProfanityRegex(String input, Long serverId);
ProfanityGroup createProfanityGroup(Long serverId, String profanityGroupName); ProfanityGroup createProfanityGroup(Long serverId, String profanityGroupName);
void deleteProfanityGroup(Long serverId, String profanityGroupName); void deleteProfanityGroup(Long serverId, String profanityGroupName);
void deleteProfanityRegex(Long serverId, String profanityGroupName, String profanityRegexName); void deleteProfanityRegex(Long serverId, String profanityGroupName, String profanityRegexName);

View File

@@ -11,6 +11,8 @@ public interface ProfanityGroupManagementService {
List<ProfanityGroup> getAllForServer(Long serverId); List<ProfanityGroup> getAllForServer(Long serverId);
ProfanityGroup createProfanityGroup(AServer server, String name); ProfanityGroup createProfanityGroup(AServer server, String name);
boolean doesProfanityGroupExist(AServer server, String name); boolean doesProfanityGroupExist(AServer server, String name);
Optional<ProfanityGroup> getProfanityGroupByIdOptional(Long profanityGroupId);
ProfanityGroup getProfanityGroupById(Long profanityGroupId);
Optional<ProfanityGroup> getProfanityGroupOptional(AServer server, String name); Optional<ProfanityGroup> getProfanityGroupOptional(AServer server, String name);
ProfanityGroup getProfanityGroup(AServer server, String name); ProfanityGroup getProfanityGroup(AServer server, String name);
void deleteProfanityGroup(ProfanityGroup profanityGroup); void deleteProfanityGroup(ProfanityGroup profanityGroup);

View File

@@ -12,4 +12,6 @@ public interface ProfanityRegexManagementService {
void deleteProfanityRegex(ProfanityGroup group, String profanityName); void deleteProfanityRegex(ProfanityGroup group, String profanityName);
boolean doesProfanityRegexExist(ProfanityGroup profanityGroup, String name); boolean doesProfanityRegexExist(ProfanityGroup profanityGroup, String name);
Optional<ProfanityRegex> getProfanityRegexOptional(ProfanityGroup profanityGroup, String name); Optional<ProfanityRegex> getProfanityRegexOptional(ProfanityGroup profanityGroup, String name);
Optional<ProfanityRegex> getProfanityRegexViaIdOptional(Long profanityRegexId);
ProfanityRegex getProfanityRegexViaId(Long profanityRegexId);
} }

View File

@@ -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. 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. 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, 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:: Changing the system configuration::
* Usage `setConfig <key> <value>` * Usage `setConfig <key> <value>`
* Description: Changes the value of this configuration identified by `key` to `value`. Some of these configurations have separate commands, but this works in general. * Description: Changes the value of this configuration identified by `key` to `value`. Some of these configurations have separate commands, but this works in general.

View File

@@ -198,4 +198,30 @@ Showing the tracked filtered invites::
Remove all or individual invites from the tracked filtered invites:: Remove all or individual invites from the tracked filtered invites::
* Usage: `removeTrackedInviteLinks [invite]` * 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. * 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. * 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 <member>`
* Description: Shows the true and false positive profanities of the given member. Also, if there any, shows the recent true positive reports.