Skip to content

Commit

Permalink
Migrate to a Groovy config DSL
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt committed May 27, 2024
1 parent a0105c2 commit 6993f33
Show file tree
Hide file tree
Showing 51 changed files with 1,164 additions and 427 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ run
bot_logs/

out/
lib/
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ compileJava {
options.compilerArgs.add('--enable-preview')
}

evaluationDependsOn(':config')

dependencies {
implementation project(':config')

implementation group: 'com.github.matyrobbrt', name: 'JDA-Chewtils', version: "${project.jda_chewtils_version}"
implementation group: 'net.dv8tion', name: 'JDA', version: "${project.jda_version}"
implementation group: 'com.google.guava', name: 'guava', version: "${project.guava_version}"
Expand Down
14 changes: 14 additions & 0 deletions config/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
id 'groovy'
id 'java-library'
}

repositories {
mavenCentral()
}

dependencies {
api "org.apache.groovy:groovy:${project.groovy_version}"
api "org.apache.groovy:groovy-contracts:${project.groovy_version}"
implementation group: 'org.kohsuke', name: 'github-api', version: project.ghapi_version
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package net.neoforged.camelot.config

import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import net.neoforged.camelot.config.module.ModuleConfiguration

/**
* The class holding Camelot's configuration, that is represented in a Groovy DSL.
*/
@CompileStatic
class CamelotConfig {
static CamelotConfig instance

private final Map<Class<? extends ModuleConfiguration>, ModuleConfiguration> modules

CamelotConfig(Map<Class<? extends ModuleConfiguration>, ModuleConfiguration> modules) {
this.modules = modules
}

/**
* The bot's Discord API key.
*/
String token

/**
* The prefix used by the bot for text commands
*/
String prefix

/**
* The owner of the bot - the user with this ID will be able to use owner-only commands
*/
long owner

/**
* Configure a module.
* @param type the type of the module
* @param configurator the closure that configures the module
*/
<T extends ModuleConfiguration> void module(Class<T> type, @DelegatesTo(type = 'T', strategy = Closure.DELEGATE_FIRST) @ClosureParams(value = FromString, options = 'T') Closure configurator) {
ConfigUtils.configure(module(type), configurator)
}

/**
* Get the module of the given type.
* @param type the type of the module
* @return the module configuration
*/
<T extends ModuleConfiguration> T module(Class<T> type) {
final conf = modules[type]
if (conf === null) {
throw new IllegalArgumentException("Unknown module of type $type")
}
return (T)conf
}

void validate() {
if (!token) {
throw new IllegalArgumentException('Bot API Token must be provided!')
}

modules.values().each {
if (it.enabled) {
it.validate()
}
}
}

/**
* Mark a value as a secret. Secrets will not be logged and any attempt at doing so will
* be redacted.
* @param value the secret value
* @return the secret value
*/
static String secret(String value) {
return value
}

/**
* Get the value of the environment variable with the given {@code key}.
* @param key the key of the env var
* @return the value
*/
static String env(String key) {
final value = System.getenv(key)
if (value === null) {
throw new IllegalArgumentException("Environment variable ${key} not found!")
}
return value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.neoforged.camelot.config

import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString

@CompileStatic
class ConfigUtils {
static <T> T configure(T object, @DelegatesTo(type = 'T', strategy = Closure.DELEGATE_FIRST) @ClosureParams(value = FromString, options = 'T') Closure closure) {
closure.setResolveStrategy(Closure.DELEGATE_FIRST)
closure.setDelegate(object)
closure.call(object)
return object
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.neoforged.camelot.config

import groovy.transform.CompileStatic

@CompileStatic
class Extensions {
static void camelot(Script obj, @DelegatesTo(value = CamelotConfig, strategy = Closure.DELEGATE_FIRST) Closure closure) {
ConfigUtils.configure(CamelotConfig.instance, closure)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.neoforged.camelot.config

import groovy.contracts.Requires
import groovy.transform.CompileStatic

/**
* Base class for configuring Mail service clients.
*/
@CompileStatic
class MailConfiguration {
/**
* Mail service configuration
*/
Map<String, ?> mailProperties

/**
* The username to use to connect to the mail server
*/
String username

/**
* The password to use to connect to the mail server
*/
String password

/**
* The email to send as
*/
String sendAs

@Requires({ username && password && sendAs })
void validate() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.neoforged.camelot.config

import groovy.contracts.Requires
import groovy.transform.CompileStatic

/**
* Base class for configuring OAuth clients.
*/
@CompileStatic
class OAuthConfiguration {
/**
* The ID of the OAuth app
*/
String clientId

/**
* The secret of the OAuth app
*/
String clientSecret

@Requires({ clientId && clientSecret })
void validate() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.neoforged.camelot.config.module

import groovy.transform.CompileStatic
import groovy.transform.NamedParam
import net.neoforged.camelot.config.ConfigUtils
import net.neoforged.camelot.config.MailConfiguration
import net.neoforged.camelot.config.OAuthConfiguration

/**
* Module for ban appeals.
*
* <p>Disabled by default.
*/
@CompileStatic
class BanAppeals extends ModuleConfiguration {
{
enabled = false
}

/**
* A guild->channel map of channels to send appeals to.
*/
Map<Long, Long> appealsChannels = [:]

/**
* Configure the appeals channel of a guild.
* Example: {@code appealsChannel(guild: 123L, channel: 124L)}
* @param args the arguments. Must have a guild and a channel parameter
*/
void appealsChannel(@NamedParam(value = 'guild', type = Long, required = true) @NamedParam(value = 'channel', type = Long, required = true) Map args) {
if (!args.guild) {
throw new IllegalArgumentException('Missing mandatory guild parameter')
} else if (!args.channel) {
throw new IllegalArgumentException('Missing mandatory channel parameter')
}
appealsChannels[args.guild as long] = args.channel as long
}

final OAuthConfiguration discordAuth = new OAuthConfiguration()

/**
* Configure the Discord OAuth client
*/
void discordAuth(@DelegatesTo(value = OAuthConfiguration, strategy = Closure.DELEGATE_FIRST) Closure config) {
ConfigUtils.configure(discordAuth, config)
}

final MailConfiguration mail = new MailConfiguration()

/**
* Configure the mail service
*/
void mail(@DelegatesTo(value = MailConfiguration, strategy = Closure.DELEGATE_FIRST) Closure config) {
ConfigUtils.configure(mail, config)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.neoforged.camelot.config.module

import groovy.transform.CompileStatic

/**
* The module that allows users to set up custom pings.
* Custom pings will send users a DM when a message matches a regex they've configured.
*/
@CompileStatic
class CustomPings extends ModuleConfiguration {
/**
* The channel in which to create ping private threads if a member does not have DMs enabled.
*/
long pingThreadsChannel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.neoforged.camelot.config.module

import groovy.transform.CompileStatic
import org.kohsuke.github.GitHub

/**
* Module for file previews.
* <p>
* If enabled, messages containing attachments with specific suffixes will have a reaction added by the bot.
* If the reaction is clicked by another user, a Gist will be created from the attachments of the message.
*/
@CompileStatic
class FilePreview extends ModuleConfiguration implements GHAuth {
/**
* The GitHub instance used to authenticate to create gists
*/
GitHub auth
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.neoforged.camelot.config.module

import groovy.contracts.Requires
import groovy.transform.CompileStatic
import net.neoforged.camelot.config.ConfigUtils
import org.kohsuke.github.GHApp
import org.kohsuke.github.GHAppInstallation
import org.kohsuke.github.GitHub
import org.kohsuke.github.GitHubBuilder

import java.nio.file.Files
import java.nio.file.Path
import java.util.function.Function

@CompileStatic
interface GHAuth {
/**
* Authenticate to GitHub using an application
*/
default GitHub appAuthentication(@DelegatesTo(value = AppAuthBuilder, strategy = Closure.DELEGATE_FIRST) Closure configurator) {
return AppAuthBuilder.appProvider.apply(ConfigUtils.configure(new AppAuthBuilder(), configurator))
}

/**
* Authenticate to GitHub using a PAT.
*/
default GitHub patAuthentication(String pat) {
return new GitHubBuilder()
.withJwtToken(pat)
.build()
}

static class AppAuthBuilder {
static Function<AppAuthBuilder, GitHub> appProvider

/**
* The ID of the app.
*/
String appId

/**
* The private key of the app
*/
String privateKey

/**
* The installation of the app
*/
String installation

/**
* Read text from a file
*/
String readFile(String path) {
return Files.readString(Path.of(path))
}

/**
* Organization-based installation
*/
String organization(String name) {
return name
}

/**
* Repository-based installation
*/
String repository(String owner, String name) {
return owner + '/' + name
}

@Requires({ appId && privateKey && installation })
Function<GHApp, GHAppInstallation> build() {
return { GHApp it ->
if (installation.contains('/')) {
return it.getInstallationByRepository(installation.split('/')[0], installation.split('/')[1])
} else {
return it.getInstallationByOrganization(installation)
}
}
}
}
}
Loading

0 comments on commit 6993f33

Please sign in to comment.