[AB-277] adding report mechanism via reactions

This commit is contained in:
Sheldan
2021-05-30 21:00:49 +02:00
parent d69f597663
commit 55e0879e06
30 changed files with 714 additions and 8 deletions

View File

@@ -0,0 +1,67 @@
package dev.sheldan.abstracto.moderation.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.database.AEmote;
import dev.sheldan.abstracto.core.models.listener.ReactionAddedModel;
import dev.sheldan.abstracto.core.service.EmoteService;
import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.ReactionService;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.service.ReactionReportService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ReactionReportListener implements AsyncReactionAddedListener {
@Autowired
private EmoteService emoteService;
@Autowired
private ReactionService reactionService;
@Autowired
private MemberService memberService;
@Autowired
private ReactionReportService reactionReportService;
@Override
public DefaultListenerResult execute(ReactionAddedModel model) {
CachedMessage cachedMessage = model.getMessage();
if(model.getUserReacting().getUserId().equals(cachedMessage.getAuthor().getAuthorId())) {
return DefaultListenerResult.IGNORED;
}
if(model.getMemberReacting().getUser().isBot()) {
return DefaultListenerResult.IGNORED;
}
Long serverId = model.getServerId();
AEmote aEmote = emoteService.getEmoteOrDefaultEmote(ReactionReportService.REACTION_REPORT_EMOTE_KEY, serverId);
if(emoteService.isReactionEmoteAEmote(model.getReaction().getReactionEmote(), aEmote)) {
memberService.retrieveMemberInServer(model.getUserReacting())
.thenCompose(member -> reactionService.removeReactionFromMessage(model.getReaction(), cachedMessage, member.getUser()))
.thenAccept(unused -> log.info("Removed report reaction on message {} in server {} in channel {}.", cachedMessage.getMessageId(), serverId, cachedMessage.getChannelId()));
log.info("User {} in server {} reacted to report a message {} from channel {}.",
model.getUserReacting().getUserId(), model.getServerId(), cachedMessage.getMessageId(), cachedMessage.getChannelId());
reactionReportService.createReactionReport(cachedMessage, model.getUserReacting()).exceptionally(throwable -> {
log.error("Failed to create reaction report in server {} on message {} in channel {}.", serverId, cachedMessage.getMessageId(), cachedMessage.getChannelId(), throwable);
return null;
});
return DefaultListenerResult.PROCESSED;
} else {
return DefaultListenerResult.IGNORED;
}
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.REPORT_REACTIONS;
}
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.moderation.repository;
import dev.sheldan.abstracto.moderation.model.database.ModerationUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ModerationUserRepository extends JpaRepository<ModerationUser, Long> {
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.moderation.repository;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.moderation.model.database.ReactionReport;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.Optional;
@Repository
public interface ReactionReportRepository extends JpaRepository<ReactionReport, Long> {
Optional<ReactionReport> findByReportedUserAndCreatedLessThan(AUserInAServer aUserInAServer, Instant maxCreated);
}

View File

@@ -0,0 +1,121 @@
package dev.sheldan.abstracto.moderation.service;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.PostTargetService;
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.moderation.config.posttarget.ReactionReportPostTarget;
import dev.sheldan.abstracto.moderation.model.database.ModerationUser;
import dev.sheldan.abstracto.moderation.model.database.ReactionReport;
import dev.sheldan.abstracto.moderation.model.template.listener.ReportReactionNotificationModel;
import dev.sheldan.abstracto.moderation.service.management.ModerationUserManagementService;
import dev.sheldan.abstracto.moderation.service.management.ReactionReportManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class ReactionReportServiceBean implements ReactionReportService {
@Autowired
private PostTargetService postTargetService;
@Autowired
private TemplateService templateService;
@Autowired
private ReactionReportManagementService reactionReportManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ConfigService configService;
@Autowired
private ChannelService channelService;
@Autowired
private ModerationUserManagementService moderationUserManagementService;
@Autowired
private ReactionReportServiceBean self;
private static final String REACTION_REPORT_TEMPLATE_KEY = "reactionReport_notification";
@Override
public CompletableFuture<Void> createReactionReport(CachedMessage reportedMessage, ServerUser reporter) {
AUserInAServer reportedUser = userInServerManagementService.loadOrCreateUser(reportedMessage.getAuthorAsServerUser());
AUserInAServer reportingUser = userInServerManagementService.loadOrCreateUser(reporter);
Optional<ModerationUser> moderationUserOptional = moderationUserManagementService.findModerationUser(reportingUser);
Long serverId = reporter.getServerId();
Long cooldownSeconds = configService.getLongValueOrConfigDefault(REACTION_REPORT_COOLDOWN, serverId);
Duration maxAge = Duration.of(cooldownSeconds, ChronoUnit.SECONDS);
if(moderationUserOptional.isPresent()) {
ModerationUser reporterModerationUser = moderationUserOptional.get();
Instant minAllowedReportTime = Instant.now().minus(maxAge);
if(reporterModerationUser.getLastReportTimeStamp() != null && reporterModerationUser.getLastReportTimeStamp().isAfter(minAllowedReportTime)) {
log.info("User {} in server {} reported user {} within the cooldown. Ignoring.", reporter.getUserId(), serverId, reportedMessage.getAuthor().getAuthorId());
return CompletableFuture.completedFuture(null);
}
}
log.info("User {} in server {} reported user {}..", reporter.getUserId(), serverId, reportedMessage.getAuthor().getAuthorId());
Optional<ReactionReport> recentReportOptional = reactionReportManagementService.findRecentReactionReportAboutUser(reportedUser, maxAge);
if(!recentReportOptional.isPresent()) {
ReportReactionNotificationModel model = ReportReactionNotificationModel
.builder()
.reportCount(1)
.reportedMessage(reportedMessage)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(REACTION_REPORT_TEMPLATE_KEY, model, serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, ReactionReportPostTarget.REACTION_REPORTS, serverId);
return FutureUtils.toSingleFutureGeneric(messageFutures)
.thenAccept(unused -> self.createReactionReportInDb(reportedMessage, messageFutures.get(0).join(), reporter));
} else {
ReactionReport report = recentReportOptional.get();
log.info("Report is already present in channel {} with message {}. Updating field.", report.getReportChannel().getId(), report.getReportMessageId());
report.setReportCount(report.getReportCount() + 1);
TextChannel reportTextChannel = channelService.getTextChannelFromServer(serverId, report.getReportChannel().getId());
return channelService.editFieldValueInMessage(reportTextChannel, report.getReportMessageId(), 0, report.getReportCount().toString())
.thenAccept(message -> self.updateModerationUserReportCooldown(reporter));
}
}
@Transactional
public void createReactionReportInDb(CachedMessage cachedMessage, Message reportMessage, ServerUser reporter) {
log.info("Creation reaction report in message {} about message {} in database.", reportMessage.getIdLong(), cachedMessage.getMessageId());
reactionReportManagementService.createReactionReport(cachedMessage, reportMessage);
updateModerationUserReportCooldown(reporter);
}
@Transactional
public void updateModerationUserReportCooldown(ServerUser reporter) {
AUserInAServer reporterAUserInServer = userInServerManagementService.loadOrCreateUser(reporter);
Optional<ModerationUser> optionalModerationUser = moderationUserManagementService.findModerationUser(reporterAUserInServer);
Instant reportTimeStamp = Instant.now();
if(optionalModerationUser.isPresent()) {
log.info("Updating last report time of user {}.", reporter.getUserId());
optionalModerationUser.get().setLastReportTimeStamp(reportTimeStamp);
} else {
log.info("Creating new moderation user instance for user {} to track report cooldowns.", reporter.getUserId());
moderationUserManagementService.createModerationUserWithReportTimeStamp(reporterAUserInServer, reportTimeStamp);
}
}
}

View File

@@ -0,0 +1,40 @@
package dev.sheldan.abstracto.moderation.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.moderation.model.database.ModerationUser;
import dev.sheldan.abstracto.moderation.repository.ModerationUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Optional;
@Component
public class ModerationUserManagementServiceBean implements ModerationUserManagementService {
@Autowired
private ModerationUserRepository repository;
@Override
public ModerationUser createModerationUser(AUserInAServer aUserInAServer) {
ModerationUser moderationUser = ModerationUser
.builder()
.id(aUserInAServer.getUserInServerId())
.user(aUserInAServer)
.server(aUserInAServer.getServerReference())
.build();
return repository.save(moderationUser);
}
@Override
public ModerationUser createModerationUserWithReportTimeStamp(AUserInAServer aUserInAServer, Instant reportTime) {
ModerationUser moderationUser = createModerationUser(aUserInAServer);
moderationUser.setLastReportTimeStamp(reportTime);
return moderationUser;
}
@Override
public Optional<ModerationUser> findModerationUser(AUserInAServer aUserInAServer) {
return repository.findById(aUserInAServer.getUserInServerId());
}
}

View File

@@ -0,0 +1,53 @@
package dev.sheldan.abstracto.moderation.service.management;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.moderation.model.database.ReactionReport;
import dev.sheldan.abstracto.moderation.repository.ReactionReportRepository;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
@Component
public class ReactionReportManagementServiceBean implements ReactionReportManagementService {
@Autowired
private ReactionReportRepository repository;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ChannelManagementService channelManagementService;
@Override
public Optional<ReactionReport> findRecentReactionReportAboutUser(AUserInAServer aUserInAServer, Duration maxAge) {
Instant maxCreation = Instant.now().minus(maxAge);
return repository.findByReportedUserAndCreatedLessThan(aUserInAServer, maxCreation);
}
@Override
public ReactionReport createReactionReport(CachedMessage reportedMessage, Message reportMessage) {
AChannel reportChannel = channelManagementService.loadChannel(reportMessage.getTextChannel());
AChannel reportedChannel = channelManagementService.loadChannel(reportedMessage.getChannelId());
AUserInAServer reportedUser = userInServerManagementService.loadOrCreateUser(reportedMessage.getAuthorAsServerUser());
ReactionReport report = ReactionReport
.builder()
.reportChannel(reportChannel)
.reportedChannel(reportedChannel)
.reportCount(1)
.reportedMessageId(reportedMessage.getMessageId())
.reportMessageId(reportMessage.getIdLong())
.reportedUser(reportedUser)
.server(reportedUser.getServerReference())
.build();
return repository.save(report);
}
}

View File

@@ -26,9 +26,7 @@
<column name="muted_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="muting_channel_id" type="BIGINT" />
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
@@ -41,7 +39,7 @@
</column>
</createTable>
<addPrimaryKey columnNames="server_id, id" tableName="mute" constraintName="pk_mute" validate="true"/>
<addForeignKeyConstraint baseColumnNames="channel_id" baseTableName="mute" constraintName="fk_mute_channel" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="muting_channel_id" baseTableName="mute" constraintName="fk_mute_channel" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="muting_user_in_server_id" baseTableName="mute" constraintName="fk_mute_muting_user" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id" referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="muted_user_in_server_id" baseTableName="mute" constraintName="fk_mute_muted_user" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id" referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="mute" constraintName="fk_mute_server_id" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="server" validate="true"/>

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="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</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="feature.xml" relativeToChangelogFile="true"/>
<include file="default_emote.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,15 @@
<?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="reactionReport_default_emote-insert">
<insert tableName="default_emote">
<column name="emote_key" value="reactionReport"/>
<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="reportReactions_feature-insertion">
<insert tableName="feature">
<column name="key" value="reportReactions"/>
</insert>
</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="moderation_user-table">
<createTable tableName="moderation_user">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_moderation_user"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="latest_report" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="moderation_user" constraintName="fk_moderation_user_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS moderation_user_update_trigger ON moderation_user;
CREATE TRIGGER moderation_user_update_trigger BEFORE UPDATE ON moderation_user FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS moderation_user_insert_trigger ON moderation_user;
CREATE TRIGGER moderation_user_insert_trigger BEFORE INSERT ON moderation_user FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,60 @@
<?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="reaction_report-table">
<createTable tableName="reaction_report">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_reaction_report"/>
</column>
<column name="reported_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="report_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="reported_message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="reported_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="report_count" type="INT">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
</createTable>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="reaction_report" constraintName="fk_reaction_report_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="reported_channel_id" baseTableName="reaction_report" constraintName="fk_reaction_report_reported_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="report_channel_id" baseTableName="reaction_report" constraintName="fk_reaction_report_report_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="reported_user_in_server_id" baseTableName="reaction_report" constraintName="fk_reaction_report_reported_user"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="user_in_server_id" referencedTableName="user_in_server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS reaction_report_update_trigger ON reaction_report;
CREATE TRIGGER reaction_report_update_trigger BEFORE UPDATE ON reaction_report FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS reaction_report_insert_trigger ON reaction_report;
CREATE TRIGGER reaction_report_insert_trigger BEFORE INSERT ON reaction_report 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="moderation_user.xml" relativeToChangelogFile="true"/>
<include file="reaction_report.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -8,4 +8,5 @@
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.0-moderation/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.11-moderation/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.15/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -1,9 +1,15 @@
abstracto.systemConfigs.decayDays.name=decayDays
abstracto.systemConfigs.decayDays.longValue=90
abstracto.systemConfigs.reactionReportCooldownSeconds.name=reactionReportCooldownSeconds
abstracto.systemConfigs.reactionReportCooldownSeconds.longValue=300
abstracto.featureFlags.moderation.featureName=moderation
abstracto.featureFlags.moderation.enabled=false
abstracto.featureFlags.reportReactions.featureName=reportReactions
abstracto.featureFlags.reportReactions.enabled=false
abstracto.featureFlags.warnings.featureName=warnings
abstracto.featureFlags.warnings.enabled=false
@@ -17,12 +23,12 @@ abstracto.featureFlags.userNotes.featureName=userNotes
abstracto.featureFlags.userNotes.enabled=false
abstracto.postTargets.warnLog.name=warnLog
abstracto.postTargets.reactionReports.name=reactionReports
abstracto.postTargets.kickLog.name=kickLog
abstracto.postTargets.banLog.name=banLog
abstracto.postTargets.muteLog.name=muteLog
abstracto.postTargets.decayLog.name=decayLog
abstracto.featureModes.banLogging.featureName=moderation
abstracto.featureModes.banLogging.mode=banLogging
abstracto.featureModes.banLogging.enabled=true

View File

@@ -10,7 +10,9 @@ public enum ModerationFeatureDefinition implements FeatureDefinition {
MUTING("muting"),
AUTOMATIC_WARN_DECAY("warnDecay"),
USER_NOTES("userNotes"),
INVITE_FILTER("inviteFilter");
INVITE_FILTER("inviteFilter"),
REPORT_REACTIONS("reportReactions")
;
private final String key;

View File

@@ -0,0 +1,36 @@
package dev.sheldan.abstracto.moderation.config.feature;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import dev.sheldan.abstracto.moderation.config.posttarget.ReactionReportPostTarget;
import dev.sheldan.abstracto.moderation.service.ReactionReportService;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class ReportReactionFeatureConfig implements FeatureConfig {
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.REPORT_REACTIONS;
}
@Override
public List<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(ReactionReportPostTarget.REACTION_REPORTS);
}
@Override
public List<String> getRequiredEmotes() {
return Arrays.asList(ReactionReportService.REACTION_REPORT_EMOTE_KEY);
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(ReactionReportService.REACTION_REPORT_COOLDOWN);
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.moderation.config.posttarget;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import lombok.Getter;
@Getter
public enum ReactionReportPostTarget implements PostTargetEnum {
REACTION_REPORTS("reactionReports");
private String key;
ReactionReportPostTarget(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,39 @@
package dev.sheldan.abstracto.moderation.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;
@Entity
@Table(name="moderation_user")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class ModerationUser {
@Id
@Column(name = "id", nullable = false)
private Long id;
@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 = "latest_report")
private Instant lastReportTimeStamp;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -83,7 +83,7 @@ public class Mute implements Serializable {
* The channel in which this mute was cast
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mutingChannel")
@JoinColumn(name = "muting_channel_id")
private AChannel mutingChannel;
/**

View File

@@ -0,0 +1,52 @@
package dev.sheldan.abstracto.moderation.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.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "reaction_report")
@Getter
@Setter
@EqualsAndHashCode
public class ReactionReport {
@Id
@Column(name = "id")
private Long reportMessageId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "report_channel_id")
private AChannel reportChannel;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reported_user_in_server_id", nullable = false)
private AUserInAServer reportedUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reported_channel_id")
private AChannel reportedChannel;
@Column(name = "reported_message_id")
private Long reportedMessageId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id")
private AServer server;
@Column(name = "report_count")
private Integer reportCount;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.moderation.model.template.listener;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ReportReactionNotificationModel {
private CachedMessage reportedMessage;
private ServerUser reporter;
private Integer reportCount;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.moderation.service;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import java.util.concurrent.CompletableFuture;
public interface ReactionReportService {
String REACTION_REPORT_EMOTE_KEY = "reactionReport";
String REACTION_REPORT_COOLDOWN = "reactionReportCooldownSeconds";
CompletableFuture<Void> createReactionReport(CachedMessage reportedMessage, ServerUser reporter);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.moderation.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.moderation.model.database.ModerationUser;
import java.time.Instant;
import java.util.Optional;
public interface ModerationUserManagementService {
ModerationUser createModerationUser(AUserInAServer aUserInAServer);
ModerationUser createModerationUserWithReportTimeStamp(AUserInAServer aUserInAServer, Instant reportTime);
Optional<ModerationUser> findModerationUser(AUserInAServer aUserInAServer);
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.moderation.service.management;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.moderation.model.database.ReactionReport;
import net.dv8tion.jda.api.entities.Message;
import java.time.Duration;
import java.util.Optional;
public interface ReactionReportManagementService {
Optional<ReactionReport> findRecentReactionReportAboutUser(AUserInAServer aUserInAServer, Duration part);
ReactionReport createReactionReport(CachedMessage reportedMessage, Message reportMessage);
}