Skip to content
This repository has been archived by the owner on Nov 25, 2022. It is now read-only.

Commit

Permalink
More abstraction, a few test commands, and a ton of other changes
Browse files Browse the repository at this point in the history
Full changelog:
 * Most JavaDocs have been added; all the least-straightforward code has been documented
 * Command is now renamed to Module
 * Further abstraction of Module, new UX features, more elegant and redundant class code
   - New ModuleInfo container class for command module metadata, with a Builder included
   - Added command descriptions
   - Separated required permissions into bot permissions and user permissions
 * package-info.java for all packages and package-level categorization via @CommandPackage(CommandType)
 * CommandType is now automatically populated in ModuleInfo
 * Added 'help', 'purge', and 'claimoperator' modules
 * Operator commands no longer rely on a hardcoded ID to check operator status; use 'claimoperator' to set and change bot operator
 * User permissions are now properly calculated & checked
 * CommandType now provides Emoji icons for each type
 * Migrated property configuration from Spring to Owner
 * Added '--debug' command-line option to make lower-level logging accessible
 * CommandDispatcher now uses an ExecutorService to start command threads instead of manually creating threads
 * Module scanning & instantiation moved to separate CommandRegistry class
 * CommandContext now provides the bot's Member as well as the invoker's
 * Upgrade to Java 10 and modularize the application
   - Note: 11 is definitely preferable, since it's the current LTS, but Hibernate depends on the java.activation module
     which was removed in 11. Once I can find a way to get jlink to recognize either the module from the Java 10 JDK or
     the non-modular Maven artifact, ghost2 will be migrated to 11 immediately.
  • Loading branch information
zocat64 committed Jul 10, 2019
1 parent 4bcd895 commit 9c00f05
Show file tree
Hide file tree
Showing 33 changed files with 830 additions and 187 deletions.
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ghost2 specific files
*.db
**/*.db
**/ghost.properties
log/*.txt
**/log/*.txt

### Gradle template
.gradle
Expand Down Expand Up @@ -46,8 +46,6 @@ hs_err_pid*
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

.idea/**/*

# Gradle
.idea/**/gradle.xml
.idea/**/libraries
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![ghost2](https://github.com/cbryant02/cbryant02.github.io/raw/master/media/ghost2_banner.png)

# ghost2
[ghost](https://github.com/cbryant02/ghost)'s spritual successor, featuring magical Spring JPA-powered database code, a super-lightweight embedded H2 database, more flexible command modules, Discord4J 3, and approximately 85.4% less spaghetti.
[ghost](https://github.com/cbryant02/ghost)'s spritual successor, featuring magical Spring JPA-powered database code, a super-lightweight embedded H2 database, more flexible module modules, Discord4J 3, and approximately 85.4% less spaghetti.

Pull requests are still welcome. JavaDocs are not written yet, but the codebase isn't yet large, complicated, or spaghettified. With some knowledge of Spring, you'll find your way around fine.

Expand Down
28 changes: 24 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
plugins {
id 'java'
id 'java-library'
id 'application'
id 'org.springframework.boot' version '2.1.6.RELEASE'
}

mainClassName = "com.github.coleb1911.ghost2.Ghost2Application"
group 'com.github.cbryant02'
version '0.1'

sourceCompatibility = 1.8
targetCompatibility = 1.8
sourceCompatibility = JavaVersion.VERSION_1_10
targetCompatibility = JavaVersion.VERSION_1_10

repositories {
mavenCentral()
Expand All @@ -17,6 +19,7 @@ repositories {
dependencies {
// Misc libraries
implementation group: 'com.discord4j', name: 'discord4j-core', version: '3.0.7'
implementation group: 'org.aeonbits.owner', name: 'owner', version: '1.0.10'
implementation group: 'org.reflections', name: 'reflections', version: '0.9.11'
implementation group: 'org.tinylog', name: 'tinylog', version: '1.3.6'
implementation group: 'org.tinylog', name: 'slf4j-binding', version: '1.3.6'
Expand All @@ -31,6 +34,23 @@ dependencies {
api group: 'org.springframework.data', name: 'spring-data-jpa', version: '2.1.9.RELEASE'

// Hibernate
api group: 'org.hibernate', name: 'hibernate-core', version: '5.4.3.Final'
api group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.4.3.Final'
implementation group: 'org.hibernate', name: 'hibernate-core', version: '5.4.3.Final'
implementation group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.4.3.Final'
}

bootJar {
exclude('ghost.properties')
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
}
}

compileJava {
inputs.property("moduleName", "ghost2.main")
doFirst {
options.compilerArgs = [
'--module-path', classpath.asPath
]
classpath = files()
}
}
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#Thu May 16 08:49:33 CDT 2019
# Thu May 16 08:49:33 CDT 2019
# suppress inspection "UnusedProperty" for whole file
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Expand Down
3 changes: 1 addition & 2 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ pluginManagement {
gradlePluginPortal()
}
}
rootProject.name = 'ghost2'

rootProject.name = 'ghost2'
103 changes: 92 additions & 11 deletions src/main/java/com/github/coleb1911/ghost2/Ghost2Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import com.github.coleb1911.ghost2.database.repos.GuildMetaRepository;
import discord4j.core.DiscordClient;
import discord4j.core.DiscordClientBuilder;
import discord4j.core.event.domain.guild.GuildCreateEvent;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.presence.Activity;
import discord4j.core.object.presence.Presence;
import discord4j.core.object.util.Snowflake;
import org.aeonbits.owner.ConfigFactory;
import org.pmw.tinylog.Configurator;
import org.pmw.tinylog.Level;
import org.pmw.tinylog.Logger;
Expand All @@ -23,17 +25,26 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Predicate;

/**
* Application entry point
*/
@ComponentScan
@SpringBootApplication
@EnableAutoConfiguration
public class Ghost2Application implements ApplicationRunner {
private static final String MESSAGE_SET_OPERATOR = "No operator has been set for this bot instance. Use the \'claimoperator\' command to set one; until then, operator commands won't work.";
private static final String CONNECTION_ERROR = "General connection error. Check your internet connection and try again.";
private static final String CONFIG_ERROR = "ghost.properties is missing or does not contain a bot token. Read ghost2's README for info on how to set up the bot.";

private static Ghost2Application applicationInstance;
private static ConfigurableApplicationContext ctx;
private static DiscordClient client;
@Autowired
private DiscordClient client;
private long operatorId;
private GhostConfig config;
@Autowired
private CommandDispatcher dispatcher;
Expand All @@ -44,49 +55,119 @@ public static void main(String[] args) {
ctx = SpringApplication.run(Ghost2Application.class, args);
}

public static void exit(int status) {
public static Ghost2Application getApplicationInstance() {
return applicationInstance;
}

/**
* Closes all resources, logs out the bot, and terminates the application gracefully.
*
* @param status Status code
*/
public void exit(int status) {
// Log out bot
client.logout().block();

// Close Spring application context
ctx.close();
SpringApplication.exit(ctx, () -> status);

// Exit
Logger.info("exiting");
System.exit(status);
}

/**
* Starts the application.
* <br/>
* This should only be called by Spring Boot.
*
* @param args Arguments passed to the application
*/
@Override
public void run(ApplicationArguments args) {
// Set instance
applicationInstance = this;

// Fetch config
config = ConfigFactory.create(GhostConfig.class);
String token = config.token();
if (null == token) {
Logger.error(CONFIG_ERROR);
return;
}

// Set up TinyLog
String dateString = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH''mm''ss").format(LocalDateTime.now());
String logFileName = String.format("log/log_%s.txt", dateString);
Configurator.defaultConfig()
.level(Level.INFO)
.level(args.containsOption("debug") ? Level.DEBUG : Level.INFO)
.addWriter(new FileWriter(logFileName))
.writingThread(true)
.activate();

// Create client
client = new DiscordClientBuilder(config.getToken())
client = new DiscordClientBuilder(token)
.setInitialPresence(Presence.online(Activity.listening("your commands.")))
.build();

// Subscribe CommandDispatcher to MessageCreateEvents
// Send MessageCreateEvents to CommandDispatcher
client.getEventDispatcher().on(MessageCreateEvent.class)
.filter(e -> {
if (e.getMember().isPresent())
return !e.getMember().get().isBot();
return false;
})
.subscribe(dispatcher::onMessageEvent);

// Update guild database with any new guilds
client.getGuilds()
// Add any new guilds to database
client.getEventDispatcher().on(GuildCreateEvent.class)
.map(GuildCreateEvent::getGuild)
.map(Guild::getId)
.map(Snowflake::asLong)
.filter(((Predicate<Long>) guildRepo::existsById).negate())
.map(id -> new GuildMeta(id, GuildMeta.DEFAULT_PREFIX))
.subscribe(guildRepo::save);

// Get current bot operator, log notice if null
operatorId = config.operatorId();
if (operatorId == -1)
Logger.info(MESSAGE_SET_OPERATOR);

// Log in and block main thread until bot logs out
client.login()
.retry(5L)
.doOnError(throwable -> {
if (throwable instanceof IOException) {
Logger.error(CONNECTION_ERROR);
exit(1);
}
}).block();
}

/**
* Reloads the application config & all related values.
* <br/>
* Note: The application will exit if a token change occurred. Don't change it at runtime.
*/
public void reloadConfig() {
config.reload();
if (!config.token().equals(client.getConfig().getToken())) {
Logger.info("Token changed on config reload. Exiting.");
exit(0);
return;
}
operatorId = config.operatorId();
}

public DiscordClient getClient() {
return client;
}

public long getOperatorId() {
return operatorId;
}

// Block thread until bot logs out
client.login().block();
public GhostConfig getConfig() {
return config;
}
}
25 changes: 13 additions & 12 deletions src/main/java/com/github/coleb1911/ghost2/GhostConfig.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.github.coleb1911.ghost2;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import org.aeonbits.owner.Accessible;
import org.aeonbits.owner.Config;
import org.aeonbits.owner.Config.Sources;
import org.aeonbits.owner.Mutable;
import org.aeonbits.owner.Reloadable;

@Component
@PropertySource(value = {"classpath:ghost.properties"})
class GhostConfig {
@Value("${ghost.token}")
private transient String token;
@Sources("classpath:ghost.properties")
public interface GhostConfig extends Config, Accessible, Mutable, Reloadable {
@Key("ghost.token")
String token();

String getToken() {
return token;
}
}
@Key("ghost.operatorid")
@DefaultValue("-1")
Long operatorId();
}
Loading

0 comments on commit 9c00f05

Please sign in to comment.