[AB-150] creating repost detection feature including configuration and documentation

adding http and hash service
adding ability to add default emotes to a message to message service
adding message embedded listener to wrap the embedded event
adding custom channel groups which can be defined by modules, in case a change on a channel group (only created and updated) happens a listener is available in order to sync the state in dependant areas
changing command receiver re-throwing abstracto runtime exceptions in order to display them better
changing channel group parameter handler to throw an exception in case the channel group was not found
adding User in a server parameter handler
split channel not found exception to be able to differentiate between not found in database and not found in guild
changing exception handling in command received handler to handle the case for only one parameter handler future which failed (the whole single future failed, which was not reported)
changing parameter type of `removeFromChannelGroup` to AChannel in order to be able to delete channels in the database via ID
moving method to mock utils for mocking consumer
removing parameter validation from commands, as it should be done in the command received handler and parameter handlers anyway
This commit is contained in:
Sheldan
2020-12-04 00:38:18 +01:00
parent e966c710ce
commit 325264a325
249 changed files with 5310 additions and 686 deletions

View File

@@ -0,0 +1,22 @@
package dev.sheldan.abstracto.utility.config;
import dev.sheldan.abstracto.core.command.UtilityModuleInterface;
import dev.sheldan.abstracto.core.command.config.ModuleInfo;
import dev.sheldan.abstracto.core.command.config.ModuleInterface;
import org.springframework.stereotype.Component;
@Component
public class RepostDetectionModuleInterface implements ModuleInterface {
public static final String REPOST_DETECTION = "repostDetection";
@Override
public ModuleInfo getInfo() {
return ModuleInfo.builder().name(REPOST_DETECTION).description("Commands related to repost detection").build();
}
@Override
public String getParentModule() {
return UtilityModuleInterface.UTILITY;
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.utility.config.features;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class RepostDetectionFeature implements FeatureConfig {
@Override
public FeatureEnum getFeature() {
return UtilityFeature.REPOST_DETECTION;
}
@Override
public List<String> getRequiredEmotes() {
return Arrays.asList("repostMarker");
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.utility.config.features;
import dev.sheldan.abstracto.core.config.FeatureMode;
import lombok.Getter;
@Getter
public enum RepostDetectionFeatureMode implements FeatureMode {
DOWNLOAD("download"), LEADERBOARD("leaderboard");
private final String key;
RepostDetectionFeatureMode(String key) {
this.key = key;
}
}

View File

@@ -5,7 +5,8 @@ import lombok.Getter;
@Getter
public enum UtilityFeature implements FeatureEnum {
REMIND("remind"), STARBOARD("starboard"), SUGGEST("suggestion"), UTILITY("utility"), LINK_EMBEDS("link_embeds");
REMIND("remind"), STARBOARD("starboard"), SUGGEST("suggestion"), UTILITY("utility"),
LINK_EMBEDS("link_embeds"), REPOST_DETECTION("repostDetection");
private String key;

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.utility.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
public class PostedImageNotFoundException extends AbstractoRunTimeException {
public PostedImageNotFoundException(Long postedMessageId, Integer position) {
super(String.format("Posted message with id %s and position %s was not found.", postedMessageId, position));
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.utility.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.templating.Templatable;
public class RepostCheckChannelGroupNotFoundException extends AbstractoRunTimeException implements Templatable {
public RepostCheckChannelGroupNotFoundException(Long channelGroupId) {
super(String.format("Repost check channel with id %s does not exist", channelGroupId));
}
@Override
public String getTemplateName() {
return "repost_check_check_channel_not_found";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.utility.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
public class RepostNotFoundException extends AbstractoRunTimeException {
public RepostNotFoundException(Long originalPostId, Integer originalPositionId, Long userInServerId) {
super(String.format("Repost with image post id %s and position %s from user %s was not found.", originalPostId, originalPositionId, userInServerId));
}
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.utility.models;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
@Getter
@Setter
@Builder
public class RepostLeaderboardEntryModel {
private Member member;
private AUserInAServer user;
private Integer count;
private Integer rank;
}

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.utility.models;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import java.util.List;
@Getter
@Setter
@Builder
public class RepostLeaderboardModel {
private List<RepostLeaderboardEntryModel> entries;
private Guild guild;
private RepostLeaderboardEntryModel userExecuting;
private Member member;
}

View File

@@ -66,6 +66,4 @@ public class EmbeddedMessage implements Serializable {
private void onInsert() {
this.created = Instant.now();
}
}

View File

@@ -0,0 +1,64 @@
package dev.sheldan.abstracto.utility.models.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 dev.sheldan.abstracto.utility.models.database.embed.PostIdentifier;
import lombok.*;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.*;
import java.time.Instant;
import java.util.List;
@Entity
@Table(name = "posted_image")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class PostedImage {
@Getter
@ManyToOne
@JoinColumn(name = "posting_user_id", nullable = false)
private AUserInAServer poster;
@Getter
@ManyToOne
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Getter
@ManyToOne
@JoinColumn(name = "posted_channel_id", nullable = false)
private AChannel postedChannel;
@EmbeddedId
@Getter
@Setter
private PostIdentifier postId;
@Column(name = "image_hash")
private String imageHash;
@Column(name = "created")
private Instant created;
@Getter
@OneToMany(fetch = FetchType.LAZY,
orphanRemoval = true,
cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE},
mappedBy = "originalPost")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Repost> reposts;
@PrePersist
private void onInsert() {
this.created = Instant.now();
}
}

View File

@@ -0,0 +1,67 @@
package dev.sheldan.abstracto.utility.models.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.models.database.embed.RepostIdentifier;
import lombok.*;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "repost")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Repost {
@EmbeddedId
@Getter
@Setter
private RepostIdentifier repostId;
@Getter
@ManyToOne
@MapsId("userInServerId")
@JoinColumn(name = "user_in_server_id", referencedColumnName = "user_in_server_id", nullable = false)
private AUserInAServer poster;
@Getter
@ManyToOne
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Column(name = "count")
private Integer count;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns(
{
@JoinColumn(updatable = false, insertable = false, name = "message_id", referencedColumnName = "message_id"),
@JoinColumn(updatable = false, insertable = false, name = "position", referencedColumnName = "position")
})
private PostedImage originalPost;
@Column(name = "created")
private Instant created;
@PrePersist
private void onInsert() {
this.created = Instant.now();
}
@Column(name = "updated")
private Instant updated;
@PreUpdate
private void onUpdate() {
this.updated = Instant.now();
}
}

View File

@@ -0,0 +1,47 @@
package dev.sheldan.abstracto.utility.models.database;
import dev.sheldan.abstracto.core.models.database.AChannelGroup;
import lombok.*;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.*;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "repost_check_channel_group")
@Getter
@Setter
@EqualsAndHashCode
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class RepostCheckChannelGroup {
@Id
private Long id;
@OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@PrimaryKeyJoinColumn
private AChannelGroup channelGroup;
@Column(name = "enabled")
private Boolean checkEnabled;
@Column(name = "created")
private Instant created;
@PrePersist
private void onInsert() {
this.created = Instant.now();
}
@Column(name = "updated")
private Instant updated;
@PreUpdate
private void onUpdate() {
this.updated = Instant.now();
}
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.utility.models.database.embed;
import lombok.*;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Embeddable
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class PostIdentifier implements Serializable {
@Column(name = "message_id")
private Long messageId;
@Column(name = "position")
private Integer position;
}

View File

@@ -0,0 +1,22 @@
package dev.sheldan.abstracto.utility.models.database.embed;
import lombok.*;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Embeddable
@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class RepostIdentifier implements Serializable {
@Column(name = "message_id")
private Long messageId;
@Column(name = "position")
private Integer position;
@Column(name = "user_in_server_id")
private Long userInServerId;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.utility.models.database.result;
public interface RepostLeaderboardResult {
Long getUserInServerId();
Integer getRepostCount();
Integer getRank();
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.utility.models.template.commands;
import dev.sheldan.abstracto.core.models.FullChannel;
import dev.sheldan.abstracto.utility.models.database.RepostCheckChannelGroup;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
public class RepostCheckChannelGroupDisplayModel {
private RepostCheckChannelGroup channelGroup;
private List<FullChannel> channels;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.utility.models.template.commands;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
public class RepostCheckChannelsModel {
private List<RepostCheckChannelGroupDisplayModel> repostCheckChannelGroups;
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import net.dv8tion.jda.api.entities.Guild;
public interface PostedImageService {
void purgePostedImages(AUserInAServer member);
void purgePostedImages(Guild guild);
}

View File

@@ -0,0 +1,29 @@
package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AChannelGroup;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.utility.models.database.RepostCheckChannelGroup;
import net.dv8tion.jda.api.entities.TextChannel;
import java.util.List;
public interface RepostCheckChannelService {
void setRepostCheckEnabledForChannelGroup(AChannelGroup channelGroup);
void setRepostCheckEnabledForChannelGroup(RepostCheckChannelGroup channelGroup);
void setRepostCheckDisabledForChannelGroup(AChannelGroup channelGroup);
void setRepostCheckDisabledForChannelGroup(RepostCheckChannelGroup channelGroup);
boolean duplicateCheckEnabledForChannel(TextChannel textChannel);
boolean duplicateCheckEnabledForChannel(AChannel channel);
List<RepostCheckChannelGroup> getRepostCheckChannelGroupsForServer(AServer server);
List<RepostCheckChannelGroup> getRepostCheckChannelGroupsForServer(Long serverId);
List<RepostCheckChannelGroup> getChannelGroupsWithEnabledCheck(AServer server);
List<RepostCheckChannelGroup> getChannelGroupsWithEnabledCheck(Long serverId);
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.models.RepostLeaderboardEntryModel;
import dev.sheldan.abstracto.utility.models.database.PostedImage;
import net.dv8tion.jda.api.entities.*;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public interface RepostService {
boolean isRepost(Message message, MessageEmbed messageEmbed, Integer embedIndex);
Optional<PostedImage> getRepostFor(Message message, MessageEmbed messageEmbed, Integer embedIndex);
boolean isRepost(Message message, Message.Attachment attachment, Integer index);
Optional<PostedImage> getRepostFor(Message message, Message.Attachment attachment, Integer index);
String calculateHashForPost(String url, Long serverId);
void processMessageAttachmentRepostCheck(Message message);
void processMessageEmbedsRepostCheck(List<MessageEmbed> embeds, Message message);
CompletableFuture<List<RepostLeaderboardEntryModel>> retrieveRepostLeaderboard(Guild guild, Integer page);
void purgeReposts(AUserInAServer userInAServer);
void purgeReposts(Guild guild);
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.utility.service.management;
import dev.sheldan.abstracto.core.models.AServerAChannelAUser;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.models.database.PostedImage;
import net.dv8tion.jda.api.entities.Message;
import java.util.List;
import java.util.Optional;
public interface PostedImageManagement {
PostedImage createPost(AServerAChannelAUser creation, Message source, String hash, Integer index);
boolean postWitHashExists(String hash, AServer server);
Optional<PostedImage> getPostWithHash(String hash, AServer server);
boolean messageHasBeenCovered(Long messageId);
boolean messageEmbedsHaveBeenCovered(Long messageId);
List<PostedImage> getAllFromMessage(Long messageId);
Optional<PostedImage> getPostFromMessageAndPositionOptional(Long messageId, Integer position);
PostedImage getPostFromMessageAndPosition(Long messageId, Integer position);
void removePostedImagesOf(AUserInAServer aUserInAServer);
void removedPostedImagesIn(AServer aServer);
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.utility.service.management;
import dev.sheldan.abstracto.core.models.database.AChannelGroup;
import dev.sheldan.abstracto.utility.models.database.RepostCheckChannelGroup;
import java.util.Optional;
public interface RepostCheckChannelGroupManagement {
RepostCheckChannelGroup loadRepostChannelGroupById(Long channelGroupId);
Optional<RepostCheckChannelGroup> loadRepostChanelGroupByIdOptional(Long channelGroupId);
boolean repostCheckChannelGroupExists(Long channelGroupId);
Optional<RepostCheckChannelGroup> loadRepostChannelGroupByChannelGroupOptional(AChannelGroup channelGroup);
RepostCheckChannelGroup loadRepostChannelGroupByChannelGroup(AChannelGroup channelGroup);
RepostCheckChannelGroup createRepostCheckChannelGroup(AChannelGroup channelGroup);
void deleteRepostCheckChannelGroup(AChannelGroup channelGroup);
}

View File

@@ -0,0 +1,22 @@
package dev.sheldan.abstracto.utility.service.management;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.models.database.PostedImage;
import dev.sheldan.abstracto.utility.models.database.Repost;
import dev.sheldan.abstracto.utility.models.database.result.RepostLeaderboardResult;
import java.util.List;
import java.util.Optional;
public interface RepostManagementService {
Repost createRepost(PostedImage postedImage, AUserInAServer poster);
Repost setRepostCount(PostedImage postedImage, AUserInAServer poster, Integer newCount);
Repost findRepost(PostedImage postedImage, AUserInAServer poster);
Optional<Repost> findRepostOptional(PostedImage postedImage, AUserInAServer poster);
List<RepostLeaderboardResult> findTopRepostingUsersOfServer(AServer server, Integer page, Integer pageSize);
List<RepostLeaderboardResult> findTopRepostingUsersOfServer(Long serverId, Integer page, Integer pageSize);
RepostLeaderboardResult getRepostRankOfUser(AUserInAServer aUserInAServer);
void deleteRepostsFromUser(AUserInAServer aUserInAServer);
void deleteRepostsFromServer(AServer server);
}