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

Commit 9ae8b88

Browse files
committed
Added config system and slash command stuff again! WOW
1 parent d764422 commit 9ae8b88

16 files changed

+689
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea/
22
.gradle/
33
build/
4+
/config/

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,15 @@
22
The second iteration of the general-purpose bot for managing the Java Discord server.
33

44
**Currently very-much a work-in-progress. Do not commit to `main` without a pull request!**.
5+
6+
## Running the Bot
7+
The bot requires a few dependencies, such as PostgreSQL and MongoDB. You can either install your own instances of these services, or use the included `docker-compose.yaml` file to boot up all of them in docker containers.
8+
9+
To do this, execute the following command from the terminal:
10+
```bash
11+
docker-compose -p javabot up
12+
```
13+
14+
For your convenience, the docker-compose file also includes admin tools that can be useful for debugging.
15+
- MongoExpress is available at [localhost:5050](http://localhost:5050)
16+
- PgAdmin is available at [locahost:5051](http://localhost:5051)

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ dependencies {
2121
// Persistence Dependencies
2222
implementation 'org.mongodb:mongodb-driver:3.12.10'
2323
implementation 'org.postgresql:postgresql:42.3.0'
24+
implementation 'com.zaxxer:HikariCP:5.0.0'
2425

2526
// Lombok annotations
2627
compileOnly 'org.projectlombok:lombok:1.18.22'
2728
annotationProcessor 'org.projectlombok:lombok:1.18.22'
2829
testCompileOnly 'org.projectlombok:lombok:1.18.22'
2930
testAnnotationProcessor 'org.projectlombok:lombok:1.18.22'
3031

32+
// Logging
33+
implementation 'ch.qos.logback:logback-classic:1.2.6'
34+
3135
// JUnit tests
3236
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
3337
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'

docker-compose.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
version: "3.9"
2+
3+
services:
4+
# MongoDB No-SQL Document Database
5+
mongo:
6+
image: mongo
7+
container_name: javabot_mongodb
8+
restart: always
9+
ports:
10+
- "27171:27017"
11+
environment:
12+
MONGO_INITDB_ROOT_USERNAME: root
13+
MONGO_INITDB_ROOT_PASSWORD: example
14+
# Web-Based Admin Tool for MongoDB. Connect via http://localhost:5050
15+
mongo-express:
16+
image: mongo-express
17+
container_name: javabot_mongo-express
18+
restart: always
19+
ports:
20+
- "5050:8081"
21+
environment:
22+
ME_CONFIG_MONGODB_ADMINUSERNAME: root
23+
ME_CONFIG_MONGODB_ADMINPASSWORD: example
24+
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"
Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,70 @@
11
package net.javadiscord.javabot2;
22

3-
import net.javadiscord.javabot2.command.CommandDataLoader;
3+
import com.mongodb.MongoClient;
4+
import com.mongodb.MongoClientURI;
5+
import com.zaxxer.hikari.HikariConfig;
6+
import com.zaxxer.hikari.HikariDataSource;
7+
import net.javadiscord.javabot2.command.SlashCommandListener;
8+
import net.javadiscord.javabot2.config.BotConfig;
49
import org.javacord.api.DiscordApi;
510
import org.javacord.api.DiscordApiBuilder;
6-
import org.javacord.api.interaction.SlashCommandBuilder;
711

8-
import java.util.List;
12+
import java.nio.file.Path;
13+
import java.util.concurrent.Executors;
14+
import java.util.concurrent.ScheduledExecutorService;
915

16+
/**
17+
* The main program entry point.
18+
*/
1019
public class Bot {
20+
/**
21+
* A connection pool that can be used to obtain new JDBC connections.
22+
*/
23+
public static HikariDataSource hikariDataSource;
24+
25+
/**
26+
* A thread-safe MongoDB client that can be used to interact with MongoDB.
27+
*/
28+
public static MongoClient mongo;
29+
30+
/**
31+
* The bot's configuration.
32+
*/
33+
public static BotConfig config;
34+
35+
/**
36+
* A general purpose thread pool for asynchronous tasks.
37+
*/
38+
public static ScheduledExecutorService asyncPool;
39+
1140
public static void main(String[] args) {
12-
DiscordApi api = new DiscordApiBuilder().setToken("").login().join();
13-
var commands = CommandDataLoader.load("commands/moderation.yaml");
14-
api.bulkOverwriteGlobalSlashCommands(List.of(
15-
new SlashCommandBuilder().setName("test")
16-
));
41+
initDataSources();
42+
asyncPool = Executors.newScheduledThreadPool(config.getSystems().getAsyncPoolSize());
43+
DiscordApi api = new DiscordApiBuilder().setToken(config.getSystems().getDiscordBotToken()).login().join();
44+
config.loadGuilds(api.getServers());
45+
config.flush();
46+
SlashCommandListener commandListener = new SlashCommandListener(api);
47+
api.addSlashCommandCreateListener(commandListener);
48+
}
49+
50+
/**
51+
* Initializes all the basic data sources that are needed by the bot's other
52+
* capabilities. This should be called <strong>before</strong> logging in
53+
* with the Discord API.
54+
*/
55+
private static void initDataSources() {
56+
config = new BotConfig(Path.of("config"));
57+
if (config.getSystems().getDiscordBotToken() == null || config.getSystems().getDiscordBotToken().isBlank()) {
58+
throw new IllegalStateException("Missing required Discord bot token! Please edit config/systems.json to add it, then run again.");
59+
}
60+
var hikariConfig = new HikariConfig();
61+
var hikariConfigSource = config.getSystems().getHikariConfig();
62+
hikariConfig.setJdbcUrl(hikariConfigSource.getJdbcUrl());
63+
hikariConfig.setUsername(hikariConfigSource.getUsername());
64+
hikariConfig.setPassword(hikariConfigSource.getPassword());
65+
hikariConfig.setMaximumPoolSize(hikariConfigSource.getMaximumPoolSize());
66+
hikariConfig.setConnectionInitSql(hikariConfigSource.getConnectionInitSql());
67+
hikariDataSource = new HikariDataSource(hikariConfig);
68+
mongo = new MongoClient(new MongoClientURI(config.getSystems().getMongoDatabaseUrl()));
1769
}
1870
}
Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
package net.javadiscord.javabot2.command;
22

33
import lombok.Data;
4+
import lombok.extern.slf4j.Slf4j;
5+
import net.javadiscord.javabot2.Bot;
6+
import net.javadiscord.javabot2.config.UnknownPropertyException;
7+
import org.javacord.api.entity.permission.Role;
8+
import org.javacord.api.entity.server.Server;
9+
import org.javacord.api.entity.user.User;
10+
import org.javacord.api.interaction.SlashCommandPermissionType;
11+
import org.javacord.api.interaction.SlashCommandPermissions;
12+
import org.javacord.api.interaction.SlashCommandPermissionsBuilder;
413

514
@Data
15+
@Slf4j
616
public class CommandPrivilegeConfig {
717
private String type;
818
private boolean enabled = true;
919
private String id;
1020

11-
// TODO: Reimplement permission deserialization.
12-
// public SlashCommandPermissions toData(long commandId, Server server) {
13-
// if (this.type.equalsIgnoreCase(SlashCommandPermissionType.USER.name())) {
14-
// User user = server.getMemberById(this.id).orElseThrow();
15-
// return new CommandPrivilege(CommandPrivilege.Type.USER, this.enabled, member.getIdLong());
16-
// } else if (this.type.equalsIgnoreCase(CommandPrivilege.Type.ROLE.name())) {
17-
// Long roleId = null;
18-
// try {
19-
// roleId = (Long) botConfig.get(guild).resolve(this.id);
20-
// } catch (UnknownPropertyException e) {
21-
// log.error("Unknown property while resolving role id.", e);
22-
// }
23-
// if (roleId == null) throw new IllegalArgumentException("Missing role id.");
24-
// Role role = guild.getRoleById(roleId);
25-
// if (role == null) throw new IllegalArgumentException("Role could not be found for id " + roleId);
26-
// return new CommandPrivilege(CommandPrivilege.Type.ROLE, this.enabled, role.getIdLong());
27-
// }
28-
// throw new IllegalArgumentException("Invalid type.");
29-
// }
21+
public SlashCommandPermissions toData(Server server) {
22+
if (this.type.equalsIgnoreCase(SlashCommandPermissionType.USER.name())) {
23+
User user = server.getMemberById(this.id).orElseThrow();
24+
return new SlashCommandPermissionsBuilder()
25+
.setType(SlashCommandPermissionType.USER)
26+
.setId(user.getId())
27+
.setPermission(true)
28+
.build();
29+
} else if (this.type.equalsIgnoreCase(SlashCommandPermissionType.ROLE.name())) {
30+
Long roleId = null;
31+
try {
32+
roleId = (Long) Bot.config.get(server).resolve(this.id);
33+
} catch (UnknownPropertyException e) {
34+
log.error("Unknown property while resolving role id.", e);
35+
}
36+
if (roleId == null) throw new IllegalArgumentException("Missing role id.");
37+
Role role = server.getRoleById(roleId).orElseThrow();
38+
return new SlashCommandPermissionsBuilder()
39+
.setType(SlashCommandPermissionType.ROLE)
40+
.setId(role.getId())
41+
.setPermission(true)
42+
.build();
43+
}
44+
throw new IllegalArgumentException("Invalid permission type.");
45+
}
3046
}
Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
package net.javadiscord.javabot2.command;
22

3+
import lombok.extern.slf4j.Slf4j;
34
import org.javacord.api.DiscordApi;
45
import org.javacord.api.entity.message.MessageFlag;
56
import org.javacord.api.event.interaction.SlashCommandCreateEvent;
7+
import org.javacord.api.interaction.ServerSlashCommandPermissionsBuilder;
68
import org.javacord.api.interaction.SlashCommandBuilder;
9+
import org.javacord.api.interaction.SlashCommandPermissions;
710
import org.javacord.api.listener.interaction.SlashCommandCreateListener;
811

9-
import java.util.Arrays;
10-
import java.util.HashMap;
11-
import java.util.List;
12-
import java.util.Map;
12+
import java.util.*;
1313
import java.util.concurrent.CompletableFuture;
1414

1515
/**
1616
* This listener is responsible for handling any incoming slash commands sent by
1717
* users in servers where the bot is active, and responding to them by calling
1818
* the appropriate {@link SlashCommandHandler}.
1919
*/
20+
@Slf4j
2021
public final class SlashCommandListener implements SlashCommandCreateListener {
2122
private final Map<Long, SlashCommandHandler> commandHandlers = new HashMap<>();
2223

2324
public SlashCommandListener(DiscordApi api) {
2425
registerSlashCommands(api, "commands/moderation.yaml")
25-
.thenAccept(commandHandlers::putAll);
26+
.thenAcceptAsync(commandHandlers::putAll);
2627
}
2728

2829
@Override
@@ -32,17 +33,16 @@ public void onSlashCommandCreate(SlashCommandCreateEvent event) {
3233
try {
3334
handler.handle(event.getSlashCommandInteraction()).respond();
3435
} catch (Exception e) {
35-
e.printStackTrace();
36+
log.error("An error occurred while handling a slash command.", e);
3637
event.getSlashCommandInteraction().createImmediateResponder()
3738
.setFlags(MessageFlag.EPHEMERAL)
38-
.append("An error occurred and the command could not be executed.")
39+
.append("An error occurred.")
3940
.respond();
4041
}
4142
} else {
4243
event.getSlashCommandInteraction().createImmediateResponder()
4344
.setFlags(MessageFlag.EPHEMERAL)
44-
.append("There is no associated handler for this command.")
45-
.append("Please contact an administrator if this error persists.")
45+
.append("There is no associated handler for this command. Please contact an administrator if this error persists.")
4646
.respond();
4747
}
4848
}
@@ -52,15 +52,20 @@ private CompletableFuture<Map<Long, SlashCommandHandler>> registerSlashCommands(
5252
var handlers = initializeHandlers(commandConfigs);
5353
List<SlashCommandBuilder> commandBuilders = Arrays.stream(commandConfigs)
5454
.map(CommandConfig::toData).toList();
55-
return api.bulkOverwriteGlobalSlashCommands(commandBuilders)
56-
.thenApply(slashCommands -> {
55+
return deleteAllSlashCommands(api)
56+
.thenComposeAsync(unused -> api.bulkOverwriteGlobalSlashCommands(commandBuilders))
57+
.thenComposeAsync(slashCommands -> {
5758
Map<Long, SlashCommandHandler> handlersById = new HashMap<>();
59+
Map<String, Long> nameToId = new HashMap<>();
5860
for (var slashCommand : slashCommands) {
5961
var handler = handlers.get(slashCommand.getName());
6062
handlersById.put(slashCommand.getId(), handler);
63+
nameToId.put(slashCommand.getName(), slashCommand.getId());
6164
}
62-
// TODO: register permissions!
63-
return handlersById;
65+
log.info("Registered all slash commands.");
66+
return updatePermissions(api, commandConfigs, nameToId)
67+
.thenRun(() -> log.info("Updated permissions for all slash commands."))
68+
.thenApplyAsync(unused -> handlersById);
6469
});
6570
}
6671

@@ -72,12 +77,38 @@ private Map<String, SlashCommandHandler> initializeHandlers(CommandConfig[] comm
7277
Class<?> handlerClass = Class.forName(commandConfig.getHandler());
7378
handlers.put(commandConfig.getName(), (SlashCommandHandler) handlerClass.getConstructor().newInstance());
7479
} catch (Exception e) {
75-
e.printStackTrace();
80+
log.error("An error occurred when trying to get new instance of a slash command handler class.", e);
7681
}
7782
} else {
78-
System.err.println("Command " + commandConfig.getName() + " does not have an associated slash command.");
83+
log.error("Command {} does not have an associated slash command handler.", commandConfig.getName());
7984
}
8085
}
8186
return handlers;
8287
}
88+
89+
private CompletableFuture<Void> deleteAllSlashCommands(DiscordApi api) {
90+
var serverDeleteFutures = api.getServers().stream()
91+
.map(server -> api.bulkOverwriteServerSlashCommands(server, List.of()))
92+
.toList();
93+
return api.bulkOverwriteGlobalSlashCommands(List.of())
94+
.thenComposeAsync(unused -> CompletableFuture.allOf(serverDeleteFutures.toArray(new CompletableFuture[0])));
95+
}
96+
97+
private CompletableFuture<Void> updatePermissions(DiscordApi api, CommandConfig[] commandConfigs, Map<String, Long> nameToId) {
98+
List<CompletableFuture<?>> permissionFutures = new ArrayList<>();
99+
for (var server : api.getServers()) {
100+
List<ServerSlashCommandPermissionsBuilder> permissionsBuilders = new ArrayList<>();
101+
for (var config : commandConfigs) {
102+
if (config.getPrivileges() != null && config.getPrivileges().length > 0) {
103+
List<SlashCommandPermissions> permissions = Arrays.stream(config.getPrivileges())
104+
.map(p -> p.toData(server)).toList();
105+
long commandId = nameToId.get(config.getName());
106+
var builder = new ServerSlashCommandPermissionsBuilder(commandId, permissions);
107+
permissionsBuilders.add(builder);
108+
}
109+
}
110+
permissionFutures.add(api.batchUpdateSlashCommandPermissions(server, permissionsBuilders));
111+
}
112+
return CompletableFuture.allOf(permissionFutures.toArray(new CompletableFuture[0]));
113+
}
83114
}

0 commit comments

Comments
 (0)