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

Commit 69d6302

Browse files
committed
Added relational-based warns and mutes.
1 parent 8b127bc commit 69d6302

File tree

21 files changed

+747
-89
lines changed

21 files changed

+747
-89
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.idea/
22
.gradle/
33
build/
4-
/javabot_config/
4+
/javabot_config/
5+
/javabot.mv.db
6+
/javabot.trace.db

build.gradle

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ targetCompatibility = 16
1111

1212
repositories {
1313
mavenCentral()
14+
maven {
15+
url "https://oss.sonatype.org/content/repositories/snapshots/"
16+
}
1417
}
1518

1619
dependencies {
17-
implementation 'org.javacord:javacord:3.3.2'
20+
implementation 'org.javacord:javacord:3.4.0-SNAPSHOT'
1821

1922
implementation 'org.yaml:snakeyaml:1.29'
2023
implementation 'com.google.code.gson:gson:2.8.9'
2124

2225
// Persistence Dependencies
2326
implementation 'org.mongodb:mongodb-driver:3.12.10'
24-
implementation 'org.postgresql:postgresql:42.3.0'
27+
implementation 'com.h2database:h2:1.4.200'
2528
implementation 'com.zaxxer:HikariCP:5.0.0'
2629

2730
// Lombok annotations

docker-compose.yaml

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,3 @@ services:
2222
ME_CONFIG_MONGODB_ADMINUSERNAME: root
2323
ME_CONFIG_MONGODB_ADMINPASSWORD: example
2424
ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
25-
# Postgresql Relational Database
26-
postgres:
27-
image: postgres:latest
28-
container_name: javabot_postgres
29-
restart: unless-stopped
30-
environment:
31-
POSTGRES_USER: javabot_dev
32-
POSTGRES_PASSWORD: javabot_dev_pass
33-
POSTGRES_DB: javabot
34-
ports:
35-
- "27172:5432"
36-
37-
# Admin site for postgres. Connect via http://localhost:5051
38-
pgadmin:
39-
image: dpage/pgadmin4
40-
container_name: javabot_pgadmin
41-
environment:
42-
PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
43-
PGADMIN_DEFAULT_PASSWORD: admin
44-
PGADMIN_CONFIG_SERVER_MODE: 'False'
45-
depends_on:
46-
- postgres
47-
ports:
48-
- "5051:80"

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,28 @@
55
import com.mongodb.client.MongoDatabase;
66
import com.mongodb.client.model.IndexOptions;
77
import com.mongodb.client.model.Indexes;
8-
import com.zaxxer.hikari.HikariConfig;
98
import com.zaxxer.hikari.HikariDataSource;
9+
import lombok.extern.slf4j.Slf4j;
1010
import net.javadiscord.javabot2.command.SlashCommandListener;
1111
import net.javadiscord.javabot2.config.BotConfig;
12+
import net.javadiscord.javabot2.db.DbHelper;
13+
import net.javadiscord.javabot2.systems.moderation.ModerationService;
1214
import org.javacord.api.DiscordApi;
1315
import org.javacord.api.DiscordApiBuilder;
1416
import org.javacord.api.entity.intent.Intent;
1517

1618
import java.nio.file.Path;
19+
import java.sql.SQLException;
20+
import java.time.ZoneOffset;
21+
import java.util.TimeZone;
1722
import java.util.concurrent.Executors;
1823
import java.util.concurrent.ScheduledExecutorService;
24+
import java.util.concurrent.TimeUnit;
1925

2026
/**
2127
* The main program entry point.
2228
*/
29+
@Slf4j
2330
public class Bot {
2431
/**
2532
* A connection pool that can be used to obtain new JDBC connections.
@@ -54,6 +61,7 @@ private Bot() {}
5461
* @param args Command-line arguments.
5562
*/
5663
public static void main(String[] args) {
64+
TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC));
5765
initDataSources();
5866
asyncPool = Executors.newScheduledThreadPool(config.getSystems().getAsyncPoolSize());
5967
DiscordApi api = new DiscordApiBuilder()
@@ -68,6 +76,7 @@ public static void main(String[] args) {
6876
"commands/moderation.yaml"
6977
);
7078
api.addSlashCommandCreateListener(commandListener);
79+
initScheduledTasks(api);
7180
}
7281

7382
/**
@@ -80,14 +89,13 @@ private static void initDataSources() {
8089
if (config.getSystems().getDiscordBotToken() == null || config.getSystems().getDiscordBotToken().isBlank()) {
8190
throw new IllegalStateException("Missing required Discord bot token! Please edit config/systems.json to add it, then run again.");
8291
}
83-
var hikariConfig = new HikariConfig();
84-
var hikariConfigSource = config.getSystems().getHikariConfig();
85-
hikariConfig.setJdbcUrl(hikariConfigSource.getJdbcUrl());
86-
hikariConfig.setUsername(hikariConfigSource.getUsername());
87-
hikariConfig.setPassword(hikariConfigSource.getPassword());
88-
hikariConfig.setMaximumPoolSize(hikariConfigSource.getMaximumPoolSize());
89-
hikariConfig.setConnectionInitSql(hikariConfigSource.getConnectionInitSql());
90-
hikariDataSource = new HikariDataSource(hikariConfig);
92+
93+
try {
94+
hikariDataSource = DbHelper.initDataSource(config);
95+
} catch (SQLException e) {
96+
log.error("Could not initialize Hikari data source.");
97+
throw new IllegalStateException(e);
98+
}
9199
mongoDb = initMongoDatabase();
92100
}
93101

@@ -99,4 +107,13 @@ private static MongoDatabase initMongoDatabase() {
99107
warnCollection.createIndex(Indexes.descending("createdAt"), new IndexOptions().unique(false));
100108
return db;
101109
}
110+
111+
private static void initScheduledTasks(DiscordApi api) {
112+
asyncPool.scheduleAtFixedRate(() -> {
113+
for (var server : api.getServers()) {
114+
new ModerationService(api, config.get(server).getModeration());
115+
// TODO: Figure out how to periodically unmute people. Consider tracking each muted person.
116+
}
117+
}, 3L, 3L, TimeUnit.MINUTES);
118+
}
102119
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.javacord.api.entity.message.embed.EmbedBuilder;
55
import org.javacord.api.event.interaction.SlashCommandCreateEvent;
66
import org.javacord.api.interaction.InteractionBase;
7+
import org.javacord.api.interaction.callback.InteractionCallbackDataFlag;
78
import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder;
89
import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater;
910

@@ -90,7 +91,7 @@ private static InteractionImmediateResponseBuilder reply(
9091
.setTimestampToNow()
9192
.setDescription(message));
9293
if (ephemeral) {
93-
responder.setFlags(MessageFlag.EPHEMERAL);
94+
responder.setFlags(InteractionCallbackDataFlag.EPHEMERAL);
9495
}
9596
return responder;
9697
}

src/main/java/net/javadiscord/javabot2/config/SystemsConfig.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ public class SystemsConfig {
3838
@Data
3939
public static class HikariConfig {
4040
private static final int DEFAULT_POOL_SIZE = 5;
41-
private String jdbcUrl = "jdbc:postgresql://localhost:27172/javabot";
42-
private String username = "javabot_dev";
43-
private String password = "javabot_dev_pass";
41+
private String jdbcUrl = "jdbc:h2:tcp://localhost:9123/./javabot";
4442
private int maximumPoolSize = DEFAULT_POOL_SIZE;
45-
private String connectionInitSql = "SET TIME ZONE 'UTC'";
4643
}
4744
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.Data;
44
import lombok.EqualsAndHashCode;
55
import net.javadiscord.javabot2.config.GuildConfigItem;
6+
import org.javacord.api.entity.channel.ServerTextChannel;
67
import org.javacord.api.entity.permission.Role;
78

89
/**
@@ -29,7 +30,25 @@ public class ModerationConfig extends GuildConfigItem {
2930
*/
3031
private int maxWarnSeverity;
3132

33+
/**
34+
* The id of the server's mute role.
35+
*/
36+
private long muteRoleId;
37+
38+
/**
39+
* The id of the channel where log messages are sent.
40+
*/
41+
private long logChannelId;
42+
3243
public Role getStaffRole() {
3344
return this.getGuild().getRoleById(staffRoleId).orElseThrow();
3445
}
46+
47+
public Role getMuteRole() {
48+
return this.getGuild().getRoleById(muteRoleId).orElseThrow();
49+
}
50+
51+
public ServerTextChannel getLogChannel() {
52+
return this.getGuild().getChannelById(logChannelId).orElseThrow().asServerTextChannel().orElseThrow();
53+
}
3554
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.javadiscord.javabot2.db;
2+
3+
import java.sql.Connection;
4+
import java.sql.SQLException;
5+
6+
@FunctionalInterface
7+
public interface ConnectionConsumer {
8+
void consume(Connection con) throws SQLException;
9+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package net.javadiscord.javabot2.db;
2+
3+
import com.zaxxer.hikari.HikariConfig;
4+
import com.zaxxer.hikari.HikariDataSource;
5+
import lombok.extern.slf4j.Slf4j;
6+
import net.javadiscord.javabot2.Bot;
7+
import net.javadiscord.javabot2.config.BotConfig;
8+
import org.h2.tools.Server;
9+
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.sql.SQLException;
15+
import java.util.Arrays;
16+
import java.util.concurrent.CompletableFuture;
17+
import java.util.regex.Pattern;
18+
19+
/**
20+
* Class that provides helper methods for dealing with the database.
21+
*/
22+
@Slf4j
23+
public class DbHelper {
24+
/**
25+
* Initializes the data source that'll be used throughout the bot to access
26+
* the database.
27+
* @param config The bot's configuration.
28+
* @return The data source.
29+
* @throws SQLException If an error occurs.
30+
*/
31+
public static HikariDataSource initDataSource(BotConfig config) throws SQLException {
32+
// Determine if we need to initialize the schema, before starting up the server.
33+
boolean shouldInitSchema = shouldInitSchema(config.getSystems().getHikariConfig().getJdbcUrl());
34+
35+
// Now that we have remembered whether we need to initialize the schema, start up the server.
36+
var server = Server.createTcpServer("-tcpPort", "9123", "-ifNotExists").start();
37+
var hikariConfig = new HikariConfig();
38+
var hikariConfigSource = config.getSystems().getHikariConfig();
39+
hikariConfig.setJdbcUrl(hikariConfigSource.getJdbcUrl());
40+
hikariConfig.setMaximumPoolSize(hikariConfigSource.getMaximumPoolSize());
41+
var ds = new HikariDataSource(hikariConfig);
42+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
43+
ds.close();
44+
server.stop();
45+
}));
46+
if (shouldInitSchema) {
47+
try {
48+
initializeSchema(ds);
49+
} catch (IOException e) {
50+
e.printStackTrace();
51+
throw new IllegalStateException(e);
52+
}
53+
}
54+
return ds;
55+
}
56+
57+
/**
58+
* Does an asynchronous database action using the bot's async pool.
59+
* @param consumer The consumer that will use a connection.
60+
* @return A future that completes when the action is complete.
61+
*/
62+
public static CompletableFuture<Void> doDbAction(ConnectionConsumer consumer) {
63+
CompletableFuture<Void> future = new CompletableFuture<>();
64+
Bot.asyncPool.submit(() -> {
65+
try (var c = Bot.hikariDataSource.getConnection()) {
66+
consumer.consume(c);
67+
future.complete(null);
68+
} catch (SQLException e) {
69+
future.completeExceptionally(e);
70+
}
71+
});
72+
return future;
73+
}
74+
75+
private static boolean shouldInitSchema(String jdbcUrl) {
76+
var p = Pattern.compile("jdbc:h2:tcp://localhost:\\d+/(.*)");
77+
var m = p.matcher(jdbcUrl);
78+
boolean shouldInitSchema = false;
79+
if (m.find()) {
80+
String dbFilePath = m.group(1) + ".mv.db";
81+
if (Files.notExists(Path.of(dbFilePath))) {
82+
log.info("Database file doesn't exist yet. Initializing schema.");
83+
shouldInitSchema = true;
84+
}
85+
} else {
86+
throw new IllegalArgumentException("Invalid JDBC URL. Should point to a file.");
87+
}
88+
return shouldInitSchema;
89+
}
90+
91+
private static void initializeSchema(HikariDataSource dataSource) throws IOException, SQLException {
92+
InputStream is = DbHelper.class.getResourceAsStream("/db/schema.sql");
93+
if (is == null) throw new IOException("Could not load schema.sql.");
94+
var queries = Arrays.stream(new String(is.readAllBytes()).split(";"))
95+
.filter(s -> !s.isBlank()).toList();
96+
try (var c = dataSource.getConnection()) {
97+
for (var query : queries) {
98+
var stmt = c.createStatement();
99+
stmt.executeUpdate(query);
100+
stmt.close();
101+
}
102+
}
103+
log.info("Successfully initialized H2 database.");
104+
}
105+
}

src/main/java/net/javadiscord/javabot2/systems/moderation/BanCommand.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ public InteractionImmediateResponseBuilder handle(SlashCommandInteraction intera
2222
.orElseThrow(ResponseException.warning("This command can only be performed in a channel."))
2323
.asServerTextChannel()
2424
.orElseThrow(ResponseException.warning("This command can only be performed in a server text channel."));
25-
var moderationService = new ModerationService(interaction.getApi(), Bot.mongoDb, Bot.config.get(channel.getServer()).getModeration());
26-
moderationService.ban(user, reason, interaction.getUser(), channel);
25+
var quiet = interaction.getOptionBooleanValueByName("quiet").orElse(false);
26+
var moderationService = new ModerationService(interaction.getApi(), Bot.config.get(channel.getServer()).getModeration());
27+
moderationService.ban(user, reason, interaction.getUser(), channel, quiet);
2728
return Responses.successBuilder(interaction)
2829
.title("User Banned")
2930
.messageFormat("User %s has been banned.", user.getMentionTag())

0 commit comments

Comments
 (0)