Skip to content

Commit

Permalink
Merge branch 'refs/heads/version/1.20.1' into version/1.19.2
Browse files Browse the repository at this point in the history
# Conflicts:
#	common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt
#	gradle.properties
  • Loading branch information
Erdragh committed May 31, 2024
2 parents 7fead4f + e2a3d28 commit 92b791d
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 85 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.5.0
- Introduce webhook chat sync, allowing for user imitation

**IMPORTANT INFO**: If you don't want to manually set the webhook URL, the
bot needs to have the MANAGE_WEBHOOKS permission

# 1.4.0
- Port to 1.20.4 and NeoForge
- Removed Forge as a supported Platform for Minecraft > 1.20.1
Expand Down
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@ while allowing true complexity for power users.
These features are the core of what this bot will do. See [the Status section](#status)
to see how much is already implemented.
- Discord and Minecraft account linking, optionally requiring this to be whitelisted
- Discord and Minecraft chat synchronization
- Discord and Minecraft chat synchronization, optionally via webhook for improved readability
- FAQ commands using Markdown files without needing to restart the server

## Dependencies

This mod has a few dependencies, some of which are not specified directly as they're technically optional:
- The Kotlin Implementation for the platform you're running. (e.g. [Kotlin For Forge](https://modrinth.com/mod/kotlin-for-forge) or [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin))
- The (Neo-)Forge Config API (Included in NeoForge, [extra mod for Fabric](https://modrinth.com/mod/forge-config-api-port))
- The (Neo-)Forge Config API (Included in Neo/Forge, [extra mod for Fabric](https://modrinth.com/mod/forge-config-api-port))

## Implementation
- [JDA](https://jda.wiki) library to communicate with the Discord API.
- Kotlin for the improved development experience over Java
- Architectury for multiplatform development
- [JetBrains Exposed](https://github.com/JetBrains/Exposed) to communicate with the Database

## Configuration
Expand All @@ -35,7 +34,7 @@ The following things will be configurable:
2. (If possible) Minecraft user imitation (The Minecraft usernames and heads will be used for messages)
3. (Only available if requiring linking for whitelist) Discord user imitation (The messages will be written under the linked Discord name)

Default: No user imitation
Default: Minecraft user imitation (if webhook available is set, otherwise no imitation)
- Managing FAQs through a command interface (default: `off`)
- Database connection. Uses an SQLite database by default

Expand All @@ -45,20 +44,20 @@ The following things will be configurable:
- [x] Reading Markdown files
- [x] Updating suggestions without restart
- [ ] Management/Creation via commands
- [ ] Chat synchronization
- [x] Chat synchronization
- [x] Minecraft to Discord
- [x] Discord to Minecraft
- [ ] User imitation on Discord
- [x] User imitation on Discord. Either uses Minecraft avatars or Discord avatars

## Running
There is no public instance of this bot/mod available. To use it, create a new Application
on the [Discord Developer Portal](https://discord.com/developers/applications) and configure it
to have the three privileged gateway intents: `PRESENCE`, `SERVER MEMBERS` and `MESSAGE CONTENT`.

Copy the bot token and store it somewhere safe (like a Password Manager) and never show it to
anybody else. To make sure the token gets read by the bot, it has to be in an [Environment Variable](https://en.wikipedia.org/wiki/Environment_variable)
`DISCORD_TOKEN` where the running Minecraft server can access it. You could for example modify a `start.sh` script
on a Unix-like system to `export` it or start the shell script with it set directly:
anybody else. To make sure the token gets read by the bot, it has to be in either an [Environment Variable](https://en.wikipedia.org/wiki/Environment_variable)
`DISCORD_TOKEN` where the running Minecraft server can access it or in the config file under the key `token`.
You could for example modify a `start.sh` script on a Unix-like system to `export` it or start the shell script with it set directly:

`startmc.sh`:
```shell
Expand All @@ -75,4 +74,9 @@ DISCORD_TOKEN=<place token here> startmc.sh # Start the script with the env vari

After starting the server, you can go into the OAuth2 URL builder on the Discord
Developer Portal and generate a URL with the `bot` and `applications.command` permissions.
Use the generated URL to have the bot join your server.
Use the generated URL to have the bot join your server.

To be able to use user imitation, which makes the chat synchronization more readable, the bot will try and create a webhook
in the configured chat synchronization channel. This means it will need to have the permission to manage webhooks. If you
don't want that you can manually create a webhook in the channel where you want the messages synchronized and set it in the
`webhook.url` option in the config for the mod.
66 changes: 36 additions & 30 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.utils.extendsFrom
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.*
import dev.architectury.plugin.ArchitectPluginExtension
import net.fabricmc.loom.api.LoomGradleExtensionAPI
import net.fabricmc.loom.task.RemapJarTask
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
// This is an Architectury repository, as such the relevant plugins are needed
Expand Down Expand Up @@ -86,6 +85,7 @@ subprojects {

// Bot dependencies
val jdaVersion: String by project
val dcWebhooksVersion: String by project
val exposedVersion: String by project
val sqliteJDBCVersion: String by project
val commonmarkVersion: String by project
Expand Down Expand Up @@ -135,13 +135,19 @@ subprojects {
arrayOf(
// Library used to communicate with Discord, see https://jda.wiki
"net.dv8tion:JDA:$jdaVersion",
// Library used for sending messages via Discord Webhooks
"club.minnced:discord-webhooks:$dcWebhooksVersion",

// Library to interact with the SQLite database,
// see: https://github.com/JetBrains/Exposed
"org.jetbrains.exposed:exposed-core:$exposedVersion",
"org.jetbrains.exposed:exposed-dao:$exposedVersion",
"org.jetbrains.exposed:exposed-jdbc:$exposedVersion",
).forEach {
implementation(it) {
exclude(module = "opus-java")
exclude(group = "org.slf4j")
}
runtimeLib(it) {
exclude(module = "opus-java")
exclude(group = "org.slf4j")
Expand All @@ -155,6 +161,7 @@ subprojects {
// on JDA and Exposed, but is already provided by the
// respective Kotlin implementation of the mod loaders
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
// Minecraft already ships with a logging system
exclude(group = "org.slf4j")
}
Expand Down Expand Up @@ -230,42 +237,41 @@ subprojects {
platformSetupLoomIde()
}

tasks {
named<ShadowJar>("shadowJar") {
archiveClassifier.set("dev-shadow")
tasks.named<ShadowJar>("shadowJar") {
// The shadowBotDep configuration was explicitly made to be shaded in, this is where that happens
configurations.clear()
configurations = listOf(shadowBotDep)

configurations = listOf(shadowBotDep, shadowCommon)
// This transforms the service files to make relocated Exposed work (see: https://github.com/JetBrains/Exposed/issues/1353)
mergeServiceFiles()

// This transforms the service files to make relocated Exposed work (see: https://github.com/JetBrains/Exposed/issues/1353)
mergeServiceFiles()
// Forge restricts loading certain classes for security reasons.
// Luckily, shadow can relocate them to a different package.
relocate("org.apache.commons.collections4", "dev.erdragh.shadowed.org.apache.commons.collections4")

// Relocating Exposed somewhere different so other mods not doing that don't run into issues (e.g. Ledger)
relocate("org.jetbrains.exposed", "dev.erdragh.shadowed.org.jetbrains.exposed")
// Relocating Exposed somewhere different so other mods not doing that don't run into issues (e.g. Ledger)
relocate("org.jetbrains.exposed", "dev.erdragh.shadowed.org.jetbrains.exposed")

// Forge restricts loading certain classes for security reasons.
// Luckily, shadow can relocate them to a different package.
relocate("org.apache.commons.collections4", "dev.erdragh.shadowed.org.apache.commons.collections4")
// Relocating jackson to prevent incompatibilities with other mods also bundling it (e.g. GroovyModLoader on Forge)
relocate("com.fasterxml.jackson", "dev.erdragh.shadowed.com.fasterxml.jackson")

// Relocating jackson to prevent incompatibilities with other mods also bundling it (e.g. GroovyModLoader on Forge)
relocate("com.fasterxml.jackson", "dev.erdragh.shadowed.com.fasterxml.jackson")
// relocate discord interaction stuff to maybe allow other discord integrations mods to work
relocate("club.minnced.discord", "dev.erdragh.shadowed.club.minnced.discord")
relocate("net.dv8tion.jda", "dev.erdragh.shadowed.net.dv8tion.jda")

exclude(".cache/**") //Remove datagen cache from jar.
exclude("**/astralbot/datagen/**") //Remove data gen code from jar.
exclude("**/org/slf4j/**")
// relocate dependencies of discord stuff
relocate("okhttp3", "dev.erdragh.shadowed.okhttp3")
relocate("okio", "dev.erdragh.shadowed.okio")
relocate("gnu.trove", "dev.erdragh.shadowed.gnu.trove")
relocate("com.iwebpp.crypto", "dev.erdragh.shadowed.com.iwebpp.crypto")
relocate("com.neovisionaries.ws", "dev.erdragh.shadowed.com.neovisionaries.ws")
relocate("org.json", "dev.erdragh.shadowed.org.json")
relocate("net.bytebuddy", "dev.erdragh.net.bytebuddy")

exclude("kotlinx/**")
exclude("_COROUTINE/**")
exclude("**/org/jetbrains/annotations/*")
exclude("**/org/intellij/**")
}
exclude("**/org/slf4j/**")

named<RemapJarTask>("remapJar") {
inputFile.set(named<ShadowJar>("shadowJar").get().archiveFile)
dependsOn("shadowJar")
// Results in the remapped jar not having any extra bit in
// its file name, identifying it as the main distribution
archiveClassifier.set(null as String?)
}
exclude("**/org/jetbrains/annotations/*")
exclude("**/org/intellij/**")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public class PlayerListMixin {

@Inject(method = "canPlayerLogin", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;)Lnet/minecraft/network/chat/MutableComponent;"), cancellable = true)
private void astralbot$returnWhiteListMessage(SocketAddress socketAddress, GameProfile gameProfile, CallbackInfoReturnable<Component> cir) {
cir.setReturnValue(WhitelistHandler.INSTANCE.writeWhitelistMessage(gameProfile));
WhitelistHandler.INSTANCE.writeWhitelistMessage(gameProfile).ifPresent(cir::setReturnValue);
}
}
5 changes: 4 additions & 1 deletion common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var minecraftHandler: MinecraftHandler? = null

var textChannel: TextChannel? = null
var guild: Guild? = null
private var jda: JDA? = null
var jda: JDA? = null
var applicationId by Delegates.notNull<Long>()

var baseDirectory: File? = null
Expand Down Expand Up @@ -97,6 +97,9 @@ private fun setupFromJDA(api: JDA) {
textChannel = ch
guild = g

// Text channel was fetched, now the minecraftHandler can fetch its webhook stuff
minecraftHandler?.updateWebhookClient()

ch.sendMessage("Server Started!").queue()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package dev.erdragh.astralbot.commands.discord

import dev.erdragh.astralbot.LOGGER
import dev.erdragh.astralbot.*
import dev.erdragh.astralbot.config.AstralBotConfig
import dev.erdragh.astralbot.config.AstralBotTextConfig
import dev.erdragh.astralbot.guild
import dev.erdragh.astralbot.handlers.WhitelistHandler
import dev.erdragh.astralbot.textChannel
import dev.erdragh.astralbot.waitForSetup
import kotlinx.coroutines.runBlocking
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.entities.Role
Expand Down Expand Up @@ -34,6 +31,7 @@ object ReloadCommand : HandledSlashCommand {
event.hook.setEphemeral(true).sendMessage(AstralBotTextConfig.GENERIC_ERROR.get()).queue()
return
}
minecraftHandler?.updateWebhookClient()
CommandHandlingListener.updateCommands(guild) { msg ->
event.hook.setEphemeral(true).sendMessage(msg).queue()
}
Expand Down Expand Up @@ -84,6 +82,9 @@ object ChatSyncCommand : HandledSlashCommand {

AstralBotConfig.DISCORD_GUILD.set(guild!!.idLong)
AstralBotConfig.DISCORD_GUILD.save()

minecraftHandler?.updateWebhookClient()

success = true
}
event.hook.setEphemeral(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object LinkCommand : Command<CommandSourceStack> {
val caller = context?.source?.playerOrException!!
if (WhitelistHandler.checkWhitelist(caller.uuid) != null) {
context.source.sendFailure(Component.literal(AstralBotTextConfig.LINK_COMMAND_ALREADY_LINKED.get()))
return 1
}
val whitelistCode = WhitelistHandler.getOrGenerateWhitelistCode(caller.uuid)
context.source.sendSuccess(Component.literal(AstralBotTextConfig.LINK_COMMAND_MESSAGE.get().replace("{{code}}", "$whitelistCode")), false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package dev.erdragh.astralbot.config
import dev.erdragh.astralbot.LOGGER
import dev.erdragh.astralbot.commands.discord.allCommands
import net.minecraftforge.common.ForgeConfigSpec
import java.net.URI
import java.net.URL
import java.net.URLDecoder
import java.nio.charset.StandardCharsets

/**
* Config for the AstralBot mod. This uses Forge's config system
Expand All @@ -20,6 +23,30 @@ object AstralBotConfig {
*/
val DISCORD_TOKEN: ForgeConfigSpec.ConfigValue<String>

/**
* Used for sending more appealing messages
*/
val WEBHOOK_URL: ForgeConfigSpec.ConfigValue<String>
/**
* Whether the configured [WEBHOOK_URL] should actually be used,
* useful in case somebody wants to temporarily disable using
* the webhook without removing the URL
*/
val WEBHOOK_ENABLED: ForgeConfigSpec.BooleanValue

/**
* URL template for getting avatars from Minecraft users
*/
val WEBHOOK_MC_AVATAR_URL: ForgeConfigSpec.ConfigValue<String>

/**
* Whether the chat messages sent via the webhook should
* imitate the sender's Discord account or their Minecraft
* account. If this is on, the linked Discord account will
* be used.
*/
val WEBHOOK_USE_LINKED: ForgeConfigSpec.BooleanValue

/**
* Whether the default whitelisting process is respected or ignored.
* Setting this to `true` will *force* every user who wants to join
Expand Down Expand Up @@ -101,6 +128,15 @@ object AstralBotConfig {
DISCORD_TOKEN = builder.comment("Discord token for the bot. Can also be supplied via DISCORD_TOKEN environment variable")
.define("token", "")

WEBHOOK_URL = builder.comment("URL to the webhook where the messages will be sent from")
.define(listOf("webhook", "url"), "")
WEBHOOK_ENABLED = builder.comment("Whether to use the configured webhook for sending messages")
.define(listOf("webhook", "enabled"), true)
WEBHOOK_USE_LINKED = builder.comment("Whether to imitate user's linked Discord accounts when sending messages from MC to DC")
.define(listOf("webhook", "useLinked"), false)
WEBHOOK_MC_AVATAR_URL = builder.comment("API that returns images based on Minecraft users. {{uuid}} and {{name}} can be used")
.define(listOf("webhook", "mcAvatarUrl"), "https://mc-heads.net/head/{{uuid}}")

REQUIRE_LINK_FOR_WHITELIST = builder.comment("Whether to require being linked to be whitelisted")
.define("requireLinkForWhitelist", false)
DISCORD_LINK = builder.comment("Link to the discord where your users can run the /link command")
Expand Down Expand Up @@ -136,15 +172,15 @@ object AstralBotConfig {
)
) {
if (it !is String) {
LOGGER.warn("$it in URL blocklist is not a String")
LOGGER.warn("$it in URI blocklist is not a String")
return@defineList false
}
// TODO: Replace with better way to check for URL
try {
URL(it)
URI(it)
return@defineList true
} catch (e: Exception) {
LOGGER.warn("Failed to parse URL on blocklist: $it", e)
LOGGER.warn("Failed to parse URI on blocklist: $it", e)
return@defineList false
}
}
Expand Down Expand Up @@ -180,12 +216,12 @@ object AstralBotConfig {
fun urlAllowed(url: String?): Boolean {
if (url == null) return true
try {
val parsedURL = URL(url)
val parsedURL = URI(url)
for (blockedURL in URL_BLOCKLIST.get()) {
if (parsedURL.host == URL(blockedURL).host) return false
if (parsedURL.host == URI(blockedURL).host) return false
}
} catch (e: Exception) {
LOGGER.warn("URL $url", e)
LOGGER.warn("URI $url", e)
return false
}
return true
Expand Down
Loading

0 comments on commit 92b791d

Please sign in to comment.