Skip to content
This repository was archived by the owner on Dec 1, 2021. It is now read-only.

Commit 87c7cad

Browse files
committed
Added ModerationService for general purpose moderation actions.
1 parent 7726e15 commit 87c7cad

File tree

6 files changed

+212
-79
lines changed

6 files changed

+212
-79
lines changed

src/main/java/net/javadiscord/javabot2/Bot.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ private static MongoDatabase initMongoDatabase() {
9696
var db = mongoClient.getDatabase("javabot");
9797
var warnCollection = db.getCollection("warn");
9898
warnCollection.createIndex(Indexes.ascending("userId"), new IndexOptions().unique(false));
99+
warnCollection.createIndex(Indexes.descending("createdAt"), new IndexOptions().unique(false));
99100
return db;
100101
}
101102
}

src/main/java/net/javadiscord/javabot2/command/Responses.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ public ResponseBuilder message(String message) {
134134
return this;
135135
}
136136

137+
/**
138+
* Sets the message of the response to a formatted string.
139+
* @param message The message's format string.
140+
* @param args The arguments to the format string.
141+
* @return The response builder.
142+
*/
143+
public ResponseBuilder messageFormat(String message, Object... args) {
144+
this.message = String.format(message, args);
145+
return this;
146+
}
147+
137148
public String getMessage() {
138149
return this.message;
139150
}

src/main/java/net/javadiscord/javabot2/config/guild/ModerationConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ public class ModerationConfig extends GuildConfigItem {
1616
*/
1717
private long staffRoleId;
1818

19+
/**
20+
* The number of days for which a user's warning may contribute to them
21+
* being removed from the server. Warnings older than this are still kept,
22+
* but ignored.
23+
*/
24+
private int warnTimeoutDays;
25+
26+
/**
27+
* The maximum total severity that a user can accrue from warnings before
28+
* being removed from the server.
29+
*/
30+
private int maxWarnSeverity;
31+
1932
public Role getStaffRole() {
2033
return this.getGuild().getRoleById(staffRoleId).orElseThrow();
2134
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package net.javadiscord.javabot2.systems.moderation;
2+
3+
4+
import com.mongodb.client.FindIterable;
5+
import com.mongodb.client.MongoDatabase;
6+
import com.mongodb.client.model.Filters;
7+
import net.javadiscord.javabot2.Bot;
8+
import net.javadiscord.javabot2.config.guild.ModerationConfig;
9+
import net.javadiscord.javabot2.systems.moderation.model.WarnSeverity;
10+
import org.bson.Document;
11+
import org.javacord.api.DiscordApi;
12+
import org.javacord.api.entity.channel.ServerTextChannel;
13+
import org.javacord.api.entity.message.embed.EmbedBuilder;
14+
import org.javacord.api.entity.user.User;
15+
16+
import java.awt.*;
17+
import java.time.Instant;
18+
import java.time.OffsetDateTime;
19+
import java.util.Map;
20+
21+
/**
22+
* This service provides methods for performing moderation actions, like banning
23+
* or warning users.
24+
*/
25+
public class ModerationService {
26+
private static final int BAN_DELETE_DAYS = 7;
27+
28+
private final DiscordApi api;
29+
private final MongoDatabase db;
30+
private final ModerationConfig config;
31+
32+
/**
33+
* Constructs the service.
34+
* @param api The API to use to interact with various discord entities.
35+
* @param db The Mongo database.
36+
* @param config The moderation config to use.
37+
*/
38+
public ModerationService(DiscordApi api, MongoDatabase db, ModerationConfig config) {
39+
this.api = api;
40+
this.db = db;
41+
this.config = config;
42+
}
43+
44+
/**
45+
* Issues a warning for the given user.
46+
* @param user The user to warn.
47+
* @param severity The severity of the warning.
48+
* @param reason The reason for this warning.
49+
* @param warnedBy The user who issued the warning.
50+
* @param channel The channel in which the warning was issued.
51+
*/
52+
public void warn(User user, WarnSeverity severity, String reason, User warnedBy, ServerTextChannel channel) {
53+
var warns = db.getCollection("warn");
54+
Instant now = Instant.now();
55+
warns.insertOne(new Document(Map.of(
56+
"userId", user.getId(),
57+
"severity", severity.name(),
58+
"reason", reason,
59+
"warnedBy", warnedBy.getId(),
60+
"createdAt", now.toEpochMilli()
61+
)));
62+
int totalSeverity = 0;
63+
for (var warnDoc : findActiveWarns(user)) {
64+
totalSeverity += WarnSeverity.getWeightOrDefault(warnDoc.getString("severity"));
65+
}
66+
var warnEmbed = buildWarnEmbed(user, severity, reason, warnedBy, now, totalSeverity);
67+
Bot.asyncPool.submit(() -> {
68+
user.openPrivateChannel().thenAcceptAsync(privateChannel -> privateChannel.sendMessage(warnEmbed));
69+
channel.sendMessage(warnEmbed);
70+
});
71+
if (totalSeverity > config.getMaxWarnSeverity()) {
72+
ban(user, "Too many warnings.", warnedBy, channel);
73+
}
74+
}
75+
76+
/**
77+
* Bans a user.
78+
* @param user The user to ban.
79+
* @param reason The reason for banning the user.
80+
* @param bannedBy The user who is responsible for banning this user.
81+
* @param channel The channel in which the ban was issued.
82+
*/
83+
public void ban(User user, String reason, User bannedBy, ServerTextChannel channel) {
84+
var banEmbed = buildBanEmbed(user, reason, bannedBy);
85+
Bot.asyncPool.submit(() -> {
86+
channel.sendMessage(banEmbed);
87+
user.openPrivateChannel()
88+
.thenComposeAsync(privateChannel -> privateChannel.sendMessage(banEmbed))
89+
.thenRunAsync(() -> channel.getServer().banUser(user, BAN_DELETE_DAYS, reason));
90+
});
91+
}
92+
93+
private FindIterable<Document> findActiveWarns(User user) {
94+
long warnTimeout = OffsetDateTime.now().minusDays(config.getWarnTimeoutDays())
95+
.toInstant().toEpochMilli();
96+
return db.getCollection("warn")
97+
.find(Filters.and(
98+
Filters.eq("userId", user.getId()),
99+
Filters.gt("createdAt", warnTimeout)
100+
));
101+
}
102+
103+
private EmbedBuilder buildWarnEmbed(User user, WarnSeverity severity, String reason, User warnedBy, Instant timestamp, int totalSeverity) {
104+
return new EmbedBuilder()
105+
.setColor(Color.ORANGE)
106+
.setTitle(String.format("%s | Warn (%d/%d)", user.getDiscriminatedName(), totalSeverity, config.getMaxWarnSeverity()))
107+
.addField("Reason", reason)
108+
.setTimestamp(timestamp)
109+
.addField("Severity", severity.name())
110+
.setFooter(warnedBy.getDiscriminatedName(), warnedBy.getAvatar());
111+
}
112+
113+
private EmbedBuilder buildBanEmbed(User user, String reason, User bannedBy) {
114+
return new EmbedBuilder()
115+
.setColor(Color.RED)
116+
.setTitle(String.format("%s | Ban", user.getDiscriminatedName()))
117+
.addField("Reason", reason)
118+
.setTimestampToNow()
119+
.setFooter(bannedBy.getDiscriminatedName(), bannedBy.getAvatar());
120+
}
121+
}
Lines changed: 8 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,34 @@
11
package net.javadiscord.javabot2.systems.moderation;
22

3-
import com.mongodb.client.model.Filters;
43
import net.javadiscord.javabot2.Bot;
54
import net.javadiscord.javabot2.command.ResponseException;
65
import net.javadiscord.javabot2.command.Responses;
76
import net.javadiscord.javabot2.command.SlashCommandHandler;
8-
import org.bson.Document;
9-
import org.javacord.api.entity.message.embed.EmbedBuilder;
10-
import org.javacord.api.entity.server.Server;
11-
import org.javacord.api.entity.user.User;
7+
import net.javadiscord.javabot2.systems.moderation.model.WarnSeverity;
128
import org.javacord.api.interaction.SlashCommandInteraction;
139
import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder;
1410

15-
import java.awt.*;
16-
import java.time.Instant;
17-
import java.util.Map;
18-
1911
/**
2012
* Command that warns a user, which is used by moderators to enforce rules.
2113
*/
2214
public class WarnCommand implements SlashCommandHandler {
23-
/**
24-
* The maximum severity that a user can reach, after which they are banned.
25-
*/
26-
public static final int MAX_SEVERITY = 100;
27-
2815
@Override
2916
public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException {
30-
var server = interaction.getServer().orElseThrow(ResponseException.warning("This command can only be used in a server."));
3117
var user = interaction.getOptionUserValueByName("user")
3218
.orElseThrow(ResponseException.warning("Missing required user."));
3319
var severityString = interaction.getOptionStringValueByName("severity")
3420
.orElseThrow(ResponseException.warning("Missing required severity."));
35-
var severity = Severity.valueOf(severityString.trim().toUpperCase());
21+
var severity = WarnSeverity.valueOf(severityString.trim().toUpperCase());
3622
var reason = interaction.getOptionStringValueByName("reason")
3723
.orElseThrow(ResponseException.warning("Missing required reason."));
38-
var warns = Bot.mongoDb.getCollection("warn");
39-
long timestamp = System.currentTimeMillis();
40-
warns.insertOne(new Document(Map.of(
41-
"userId", user.getId(),
42-
"severity", severity.name(),
43-
"reason", reason,
44-
"warnedBy", interaction.getUser().getId(),
45-
"createdAt", timestamp
46-
)));
47-
int totalSeverity = 0;
48-
for (var doc : warns.find(Filters.eq("userId", user.getId()))) {
49-
totalSeverity += Severity.valueOf(doc.getString("severity")).getWeight();
50-
}
51-
int finalTotalSeverity = totalSeverity;
52-
if (finalTotalSeverity > MAX_SEVERITY) {
53-
return banUser(interaction, user, server);
54-
} else {
55-
return warnUser(interaction, new WarnData(user, timestamp, severity, reason, interaction.getUser(), finalTotalSeverity, server));
56-
}
57-
}
58-
59-
private InteractionImmediateResponseBuilder warnUser(SlashCommandInteraction interaction, WarnData warn) {
60-
EmbedBuilder embed = buildWarnEmbed(warn);
61-
Bot.asyncPool.submit(() -> {
62-
warn.user().openPrivateChannel().thenAcceptAsync(privateChannel -> privateChannel.sendMessage(embed));
63-
interaction.getChannel().orElseThrow().sendMessage(embed);
64-
});
24+
var channel = interaction.getChannel()
25+
.orElseThrow(ResponseException.warning("Missing required channel."))
26+
.asServerTextChannel().orElseThrow(ResponseException.warning("This command can only be used in server text channels."));
27+
var moderationService = new ModerationService(interaction.getApi(), Bot.mongoDb, Bot.config.get(channel.getServer()).getModeration());
28+
moderationService.warn(user, severity, reason, interaction.getUser(), channel);
6529
return Responses.successBuilder(interaction)
6630
.title("User Warned")
67-
.message(String.format("User %s has been warned.", warn.user().getMentionTag()))
68-
.build();
69-
}
70-
71-
private EmbedBuilder buildWarnEmbed(WarnData warn) {
72-
return new EmbedBuilder()
73-
.setColor(Color.ORANGE)
74-
.setTitle(String.format("%s | Warn (%d/%d)", warn.user().getDisplayName(warn.server()), warn.totalSeverity(), MAX_SEVERITY))
75-
.addField("Reason", warn.reason())
76-
.setTimestamp(Instant.ofEpochMilli(warn.timestamp()))
77-
.addField("Severity", warn.severity().name())
78-
.setFooter(warn.warnedBy().getDisplayName(warn.server()));
79-
}
80-
81-
private InteractionImmediateResponseBuilder banUser(SlashCommandInteraction interaction, User user, Server server) {
82-
user.openPrivateChannel().thenAcceptAsync(privateChannel -> privateChannel.sendMessage("You have been banned after receiving too many warnings."));
83-
server.banUser(user, 0, "Too many warnings.");
84-
return Responses.successBuilder(interaction)
85-
.title("User Banned")
86-
.message(String.format("User %s was banned after receiving too many warnings.", user.getDisplayName(server)))
31+
.messageFormat("User %s has been warned.", user.getMentionTag())
8732
.build();
8833
}
89-
90-
private enum Severity {
91-
LOW(10), MEDIUM(20), HIGH(40);
92-
93-
private final int weight;
94-
95-
Severity(int weight) {
96-
this.weight = weight;
97-
}
98-
99-
public int getWeight() {
100-
return this.weight;
101-
}
102-
}
103-
104-
private static record WarnData(User user, long timestamp, Severity severity, String reason, User warnedBy, int totalSeverity, Server server){}
10534
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package net.javadiscord.javabot2.systems.moderation.model;
2+
3+
/**
4+
* Enum representing the different possible severities that can be chosen when
5+
* warning a user.
6+
*/
7+
public enum WarnSeverity {
8+
/**
9+
* Low severity is intended for small violations.
10+
*/
11+
LOW(10),
12+
13+
/**
14+
* Medium severity is for more egregious, but still mostly harmless violations.
15+
*/
16+
MEDIUM(20),
17+
18+
/**
19+
* High severity indicates the user did something intentionally malicious.
20+
*/
21+
HIGH(40);
22+
23+
/**
24+
* A default weight that is used as a fallback in any case where it is
25+
* impossible to obtain the actual weight for a warning, like if a warning
26+
* document contains an unknown severity value.
27+
*/
28+
public static final int DEFAULT_WEIGHT = 20;
29+
30+
private final int weight;
31+
32+
/**
33+
* Constructs the value.
34+
* @param weight The weight to use.
35+
*/
36+
WarnSeverity(int weight) {
37+
this.weight = weight;
38+
}
39+
40+
public int getWeight() {
41+
return this.weight;
42+
}
43+
44+
/**
45+
* Gets the weight for a given severity name.
46+
* @param name The name of the severity level.
47+
* @return The weight for the given severity, or {@link #DEFAULT_WEIGHT} if
48+
* no matching severity could be found.
49+
*/
50+
public static int getWeightOrDefault(String name) {
51+
for (var v : values()) {
52+
if (v.name().equalsIgnoreCase(name)) {
53+
return v.weight;
54+
}
55+
}
56+
return DEFAULT_WEIGHT;
57+
}
58+
}

0 commit comments

Comments
 (0)