[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": [
"🇷"
"🇷",
"🌱"
],
"s": [
"🇸",

View File

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

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.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";

View File

@@ -26,7 +26,7 @@
<module>webservices</module>
<module>logging</module>
<module>invite-filter</module>
<module>profanity-filter</module>
</modules>
</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>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId>
@@ -63,8 +62,6 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>