diff --git a/.travis.yml b/.travis.yml index 5948683..d1f3f5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,16 @@ jdk: - oraclejdk8 after_success: - - ./gradlew shadowJar + - ./gradlew build deploy: provider: releases api_key: secure: KJd23J+YlpBuUs+5//5BjYlSQ9mVOsYnpfanE03ZQ4E9Qp+pGpMSaH60d/CXkSkDVr32QL5jk0VRDNVl0wvTZpRJfNclQgh3fh2iPkaBDmjun0YkGHfE6VeRzJZVuIF/F34pkziEC01B7Tq2loms+RSm9pNeWgN+ulaajNENUT2J4nmb33yyKbGN9I4EdjcMCQmgdnxn/4QNXV6T3Xls9S9X7AwA9pg4WNpdACqJ0oWj2YlSWKmAdcKw0iLQtRbQfbiqj6waOLODdxieIu0KVWEVQnTdcv7ElGl9BgDrHzXBjfS+9G112TBqsNIPx60o8Z7ThR02DYPWYXrRyfvaHM0FqXrBSaUZUxfHpGn5F4CRQLZh/jOnIOZ9hHmCSDD69pcS254tVCoB+hRuS7PBstuh8iUO7VGJASb5Mv34LZYLAxfvNDAKLeY60xdz/Uiku9JW0dpLQC+FWAvFZWdQ3IvCMYRnaT1fmjDJKanYvCw31l5ypACjfhIQfnwG7FKeLdh6BJx9CG/eHV2URO4do5OCLBkuL2IYQQXqlj6jU4QpXjfWDf7XCAW3MBthjwe5lnLeNZTkcwttQqIOZdCn7Qa/zaG+HJM62tnV9uhLpj4fhaGmf7tapdUWJ5cuUUGLA3MNmSqsZ3ohF5wGlKsGwFVOb6UhoB9szmA9p7zQGpw= - file: build/DiscordBridge-*.jar + file: + - discordbridge-bukkit/build/libs/discordbridge-bukkit*.jar + - discordbridge-spigot/build/libs/discordbridge-spigot*.jar + - discordbridge-forge/build/libs/discordbridge-forge*.jargit file_glob: true on: repo: the-obsidian/DiscordBridge diff --git a/LICENSE-THIRD-PARTY.txt b/LICENSE-THIRD-PARTY.txt index 670dc41..be6603b 100644 --- a/LICENSE-THIRD-PARTY.txt +++ b/LICENSE-THIRD-PARTY.txt @@ -204,4 +204,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/NOTICE-THIRD-PARTY.txt b/NOTICE-THIRD-PARTY.txt index 218c5b3..9a4d456 100644 --- a/NOTICE-THIRD-PARTY.txt +++ b/NOTICE-THIRD-PARTY.txt @@ -2,4 +2,4 @@ pegdown Copyright (C) 2010-2011 Mathias Doenitz Based on peg-markdown - markdown in c, implemented using PEG grammar -Copyright (c) 2008 John MacFarlane (http://github.com/jgm/peg-markdown) \ No newline at end of file +Copyright (c) 2008 John MacFarlane (http://github.com/jgm/peg-markdown) diff --git a/README.md b/README.md index 73abbc6..3002c0b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ # DiscordBridge [![Build Status](https://travis-ci.org/the-obsidian/DiscordBridge.svg?branch=master)](https://travis-ci.org/the-obsidian/DiscordBridge) -Bridges chat between Discord and Minecraft (Bukkit/Spigot). +Bridges chat between Discord and Minecraft. ## Requirements * Java 8 + +Any of: * Spigot 1.12 +* Sponge 1.12 +* Forge 1.12 ## Installation 1. Download the latest release from GitHub -2. Add it to your plugins folder -3. Either run Bukkit/Spigot once to generate DiscordBridge/config.yml or create it using the guide below. +2. Add it to your plugin/mod folder +3. Either run your server once to generate DiscordBridge/config.yml or create it using the guide below. 4. All done! @@ -112,7 +116,8 @@ templates: * Anything said in Minecraft chat will be sent to your chosen Discord channel * If Multiverse-Core is installed and the `%w` tag is specified in your relay message syntax, the alias assigned to your Multiverse worlds will be displayed * Anything said in your chosen Discord channel will be sent to your Minecraft chat (if the `%w` tag is used in your relay message syntax, Discord messages will display `Discord`) -* If Dynmap is installed, anything said over Dynmap chat will be relayed to your chosen Discord channel (if the `%w` tag is used in your relay messag syntax, Dynmap messages will display `Dynmap`) +* If Dynmap is installed, anything said over Dynmap chat will be relayed to your chosen Discord channel (if the `%w` tag is used in your relay messag syntax, Dynmap messages will display `Dynmap`) (Spigot only for now) +* Uploaded images and other files in Discord will show up in Minecraft chat as clickable URLs * You can link Minecraft accounts to Discord accounts and the bot will translate display names to match where the message appears * Join / leave messages can be sent to Discord * Death messages can be sent to Discord @@ -129,6 +134,8 @@ templates: ## Permissions +***NOTE:*** Only the Spigot version supports permission nodes at this time. + - `discordbridge.discord` - ability to use any command in of the /discord subcommand tree - `discordbridge.discord.reload` - ability to reload configs and JDA - `discordbridge.discord.listmembers` - abiliyt to receive a list of members in the Discord channel @@ -158,7 +165,6 @@ templates: ## Upcoming Features -* Add support for a URL shortening service so attachment URLs aren't so flipping long * Add support for relaying embeds * Make Discord responses for certain commands return in pretty embeds * More of the 'fun' commands that literally every Discord bot has (with matching Minecraft commands!) diff --git a/build.gradle b/build.gradle index ce9bd56..96e1d96 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ import org.apache.tools.ant.filters.ReplaceTokens buildscript { - ext.kotlin_version = '1.2.0' + ext.kotlin_version = '1.2.10' repositories { mavenCentral() @@ -11,84 +11,74 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.4' } -} - -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '1.2.4' -} - -apply plugin: 'kotlin' - -group = 'gg.obsidian' -version = '3.1.0' -description = """Bridge chat between Minecraft and Discord""" -ext.url = 'https://github.com/the-obsidian/DiscordBridge' -repositories { - mavenCentral() - maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - maven { url 'http://lutece.paris.fr/nexus/content/repositories/lutece_third_party' } - maven { url 'https://github.com/DV8FromTheWorld/Maven-Repository/raw/master/repo' } - - jcenter() -} - -dependencies { - compile group: 'org.spigotmc', name: 'spigot-api', version: '1.12-R0.1-SNAPSHOT' - compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: '1.1.3-2' - compile group: 'net.dv8tion', name: 'JDA', version: '3.3.1_286' - compile group: 'com.michaelwflaherty', name: 'cleverbotapi', version: '1.0.1' - compile group: 'org.pegdown', name:'pegdown', version: '1.6.0' - compile group: 'org.json', name: 'json', version: '20160810' - - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - compile files('libraries/dynmap-api-2.5-SNAPSHOT.jar') + configurations.all { + resolutionStrategy { + force 'org.ow2.asm:asm:6.0_BETA' + } + } } -compileKotlin { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 +subprojects { + apply plugin: 'java' + apply plugin: 'kotlin' + apply plugin: 'com.github.johnrengelman.shadow' - kotlinOptions { - jvmTarget = "1.8" + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } -} -processResources { - filter ReplaceTokens, tokens: [ - 'DESCRIPTION': project.property('description'), - 'URL' : project.property('url'), - 'VERSION' : project.property('version') - ] -} + group = 'gg.obsidian' + version = '4.0.0' + description = """Bridge chat between Minecraft and Discord""" + ext.url = 'https://github.com/the-obsidian/DiscordBridge' + ext.modid = 'discordbridge-obsidian' + processResources { + filter ReplaceTokens, tokens: [ + 'DESCRIPTION': project.property('description'), + 'URL' : project.property('url'), + 'VERSION' : project.property('version'), + 'MODID' : project.property('modid') + ] + } -//noinspection GroovyAssignabilityCheck -build.finalizedBy(shadowJar) + repositories { + maven { url 'https://github.com/DV8FromTheWorld/Maven-Repository/raw/master/repo' } + jcenter() + } -shadowJar { - relocate 'org.apache', 'shadow.apache' - relocate 'org.json', 'shadow.json' - classifier 'dist' dependencies { - //noinspection GroovyAssignabilityCheck - exclude(dependency('org.spigotmc:.*:.*')) + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } - exclude '.cache' -} -task copyFinalJar(type: Copy) { - from "build/libs/${shadowJar.archiveName}" - into "build" - rename( - shadowJar.archiveName, - "${project.property('name')}-${project.property('version')}.jar" - ) -} - -shadowJar.finalizedBy(copyFinalJar) -sourceSets { - main.java.srcDirs += 'src/main/kotlin' + shadowJar { + classifier = null + dependencies { + // Kotlin Runtime + include(dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")) + + // Dependencies for JDA + include(dependency('net.dv8tion:JDA:3.3.1_286')) + include(dependency('com.squareup.okhttp3:okhttp:')) + include(dependency('com.squareup.okio:okio')) + include(dependency('com.neovisionaries:nv-websocket-client')) + include(dependency('org.json:json')) + include(dependency('org.slf4j:slf4j-api')) + include(dependency('org.slf4j:slf4j-simple')) + + // Dependency for SnakeYAML + include(dependency(group: 'org.yaml', name: 'snakeyaml', version: '1.19')) + + // Dependencies for PegDown + include(dependency(group: 'org.pegdown', name:'pegdown', version: '1.6.0')) + include(dependency(group: 'commons-lang', name:'commons-lang', version: '2.3')) + include(dependency('org.parboiled:parboiled-core')) + include(dependency('org.parboiled:parboiled-java')) + + // Dependency for CleverbotAPI + include(dependency(group: 'com.michaelwflaherty', name: 'cleverbotapi', version: '1.0.1')) + } + } } diff --git a/discordbridge-bukkit/build.gradle b/discordbridge-bukkit/build.gradle new file mode 100644 index 0000000..c56db8e --- /dev/null +++ b/discordbridge-bukkit/build.gradle @@ -0,0 +1,28 @@ +repositories { + mavenCentral() + maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} + +dependencies { + compile project(':discordbridge-core') + compile group: 'org.spigotmc', name: 'spigot-api', version: '1.12-R0.1-SNAPSHOT' + compile (group: 'org.pegdown', name:'pegdown', version: '1.6.0') + compile files('../libraries/dynmap-api-2.5-SNAPSHOT.jar') +} + +shadowJar { + dependencies { + include(project(':discordbridge-core')) + include(dependency('org.ow2.asm:asm')) + include(dependency('org.ow2.asm:asm-analysis')) + include(dependency('org.ow2.asm:asm-tree')) + include(dependency('org.ow2.asm:asm-util')) + } + relocate 'org.json', 'shadow.json' +} +build.dependsOn(shadowJar) + +artifacts { + archives shadowJar +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/BukkitDiscordBridge.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/BukkitDiscordBridge.kt new file mode 100644 index 0000000..6c5e456 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/BukkitDiscordBridge.kt @@ -0,0 +1,43 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.wrapper.DbBukkitServer +import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * The primary Plugin class that maintains the plugin's connection with Bukkit + */ +class BukkitDiscordBridge : JavaPlugin() { + private lateinit var instance: BukkitDiscordBridge + private lateinit var logger: Logger + + override fun onLoad() { + logger = LoggerFactory.getLogger("DiscordBrdige") + logger.info("Loading DiscordBridge") + } + + override fun onEnable() { + logger.info("Enabling DiscordBridge") + instance = this + val isMultiverse = server.pluginManager.getPlugin("Multiverse-Core") != null + DiscordBridge.init(DbBukkitServer(this, this.server), dataFolder, isMultiverse=isMultiverse) + + server.pluginManager.registerEvents(EventListener(), this) + + getCommand("discord").executor = EventListener() + getCommand("f").executor = EventListener() + getCommand("rate").executor = EventListener() + getCommand("8ball").executor = EventListener() + getCommand("insult").executor = EventListener() + getCommand("choose").executor = EventListener() + getCommand("talk").executor = EventListener() + getCommand("roll").executor = EventListener() + + DiscordBridge.handleServerStart() + } + + override fun onDisable() { + DiscordBridge.handleServerStop() + } +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordRCon.kt similarity index 58% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt rename to discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordRCon.kt index 8658bcf..bbd7d4e 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordRCon.kt @@ -1,7 +1,6 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge -import net.dv8tion.jda.core.entities.MessageChannel -import org.bukkit.Bukkit +import gg.obsidian.discordbridge.command.DiscordCommandSender import org.bukkit.Server import org.bukkit.command.CommandSender import org.bukkit.command.ConsoleCommandSender @@ -11,86 +10,77 @@ import org.bukkit.permissions.PermissionAttachment import org.bukkit.permissions.PermissionAttachmentInfo import org.bukkit.plugin.Plugin -class DiscordCommandSender(val channel: MessageChannel) : RemoteConsoleCommandSender { - - private val sender:ConsoleCommandSender = Bukkit.getServer().consoleSender - - init { - - } - - override fun sendMessage(message: String?) { - channel.sendMessage(message).queue() +class DiscordRCon(val discordSender: DiscordCommandSender, val base: ConsoleCommandSender) : RemoteConsoleCommandSender { + override fun sendMessage(message: String) { + discordSender.sendMessage(message) } override fun sendMessage(messages: Array?) { if (messages != null) - for (m in messages) channel.sendMessage(m) + for (m in messages) discordSender.sendMessage(m) } override fun spigot(): CommandSender.Spigot { - return sender.spigot() + return base.spigot() } override fun addAttachment(plugin: Plugin?): PermissionAttachment { - return sender.addAttachment(plugin) + return base.addAttachment(plugin) } override fun addAttachment(plugin: Plugin?, ticks: Int): PermissionAttachment { - return sender.addAttachment(plugin, ticks) + return base.addAttachment(plugin, ticks) } override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean): PermissionAttachment { - return sender.addAttachment(plugin, name, value) + return base.addAttachment(plugin, name, value) } override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean, ticks: Int): PermissionAttachment { - return sender.addAttachment(plugin, name, value, ticks) + return base.addAttachment(plugin, name, value, ticks) } override fun getEffectivePermissions(): MutableSet { - return sender.effectivePermissions + return base.effectivePermissions } override fun getName(): String { - return sender.name + return discordSender.senderName } override fun getServer(): Server { - return sender.server + return base.server } override fun hasPermission(name: String?): Boolean { - return sender.hasPermission(name) + return base.hasPermission(name) } override fun hasPermission(perm: Permission?): Boolean { - return sender.hasPermission(perm) + return base.hasPermission(perm) } override fun isOp(): Boolean { - return sender.isOp + return base.isOp } override fun isPermissionSet(name: String?): Boolean { - return sender.isPermissionSet(name) + return base.isPermissionSet(name) } override fun isPermissionSet(perm: Permission?): Boolean { - return sender.isPermissionSet(perm) + return base.isPermissionSet(perm) } override fun recalculatePermissions() { - return sender.recalculatePermissions() + return base.recalculatePermissions() } override fun removeAttachment(attachment: PermissionAttachment?) { - return sender.removeAttachment(attachment) + return base.removeAttachment(attachment) } override fun setOp(value: Boolean) { - return sender.setOp(value) + return base.setOp(value) } - - -} \ No newline at end of file +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt new file mode 100644 index 0000000..4cdac14 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt @@ -0,0 +1,52 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.wrapper.DbBukkitConsoleSender +import gg.obsidian.discordbridge.wrapper.DbBukkitPlayer +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.AsyncPlayerChatEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.dynmap.DynmapWebChatEvent + +class EventListener: Listener, CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + return if (sender is org.bukkit.entity.Player) { + val wp = DbBukkitPlayer(sender) + DiscordBridge.handleCommand(wp, command.name, args) + } else { + DiscordBridge.handleCommand(DbBukkitConsoleSender(Bukkit.getConsoleSender()), command.name, args) + } + } + + @EventHandler + fun onChat(event: AsyncPlayerChatEvent) { + event.message = DiscordBridge.handlePlayerChat(DbBukkitPlayer(event.player), event.message, event.isCancelled) + } + + @EventHandler + fun onPlayerJoin(event: PlayerJoinEvent) { + DiscordBridge.handlePlayerJoin(DbBukkitPlayer(event.player)) + } + + @EventHandler + fun onPlayerLeave(event: PlayerQuitEvent) { + DiscordBridge.handlePlayerQuit(DbBukkitPlayer(event.player)) + } + + @EventHandler + fun onPlayerDeath(event: PlayerDeathEvent) { + DiscordBridge.handlePlayerDeath(DbBukkitPlayer(event.entity.player), event.deathMessage) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onDynmapCatEvent(event: DynmapWebChatEvent) { + DiscordBridge.handleDynmapChat(event.name, event.message) + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitCommand.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitCommand.kt new file mode 100644 index 0000000..922cabd --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitCommand.kt @@ -0,0 +1,9 @@ +package gg.obsidian.discordbridge.wrapper + +import org.bukkit.command.Command + +class DbBukkitCommand(val cmd: Command) : IDbCommand { + override fun getName(): String { + return cmd.name + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitConsoleSender.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitConsoleSender.kt new file mode 100644 index 0000000..b49a964 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitConsoleSender.kt @@ -0,0 +1,22 @@ +package gg.obsidian.discordbridge.wrapper + +import org.bukkit.command.ConsoleCommandSender +import java.util.* + +class DbBukkitConsoleSender(private val bukkitConsoleSender: ConsoleCommandSender) : IDbConsoleSender { + override fun sendMessage(message: String) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun hasPermission(permission: String): Boolean { + return bukkitConsoleSender.hasPermission(permission) + } + + override fun getUUID(): UUID { + TODO("not implemented") + } + + override fun getName(): String { + return "DiscordRemote" + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitLogger.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitLogger.kt new file mode 100644 index 0000000..ce29006 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitLogger.kt @@ -0,0 +1,26 @@ +package gg.obsidian.discordbridge.wrapper + +import java.util.logging.Level +import java.util.logging.Logger + +class DbBukkitLogger(private val logger: Logger) : IDbLogger { + override fun info(message: String) { + logger.info(message) + } + + override fun warning(message: String) { + logger.warning(message) + } + + override fun warning(message: String, throwable: Throwable) { + logger.log(Level.WARNING, message, throwable) + } + + override fun severe(message: String) { + logger.severe(message) + } + + override fun severe(message: String, throwable: Throwable) { + logger.log(Level.SEVERE, message, throwable) + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitPlayer.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitPlayer.kt new file mode 100644 index 0000000..10d1ad3 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitPlayer.kt @@ -0,0 +1,32 @@ +package gg.obsidian.discordbridge.wrapper + +import org.bukkit.OfflinePlayer +import java.util.* + +// TODO: Add safety for when OfflinePlayer is not online +class DbBukkitPlayer(private val bukkitPlayer: OfflinePlayer) : IDbPlayer { + override fun hasPermission(permission: String): Boolean { + return bukkitPlayer.player.hasPermission(permission) + } + + override fun getWorld(): IDbWorld { + return DbBukkitWorld(bukkitPlayer.player.world) + } + + override fun isVanished(): Boolean { + return bukkitPlayer.player.hasMetadata("vanished") + && bukkitPlayer.player.getMetadata("vanished")[0].asBoolean() + } + + override fun getName(): String { + return bukkitPlayer.name + } + + override fun sendMessage(message: String) { + bukkitPlayer.player.sendMessage(message) + } + + override fun getUUID(): UUID { + return bukkitPlayer.uniqueId + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitScheduler.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitScheduler.kt new file mode 100644 index 0000000..3007b9d --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitScheduler.kt @@ -0,0 +1,10 @@ +package gg.obsidian.discordbridge.wrapper + +import gg.obsidian.discordbridge.BukkitDiscordBridge +import org.bukkit.scheduler.BukkitScheduler + +class DbBukkitScheduler(private val plugin: BukkitDiscordBridge, private val bukkitScheduler: BukkitScheduler) : IDbScheduler { + override fun runAsyncTask(task: Runnable) { + bukkitScheduler.runTaskAsynchronously(plugin, task) + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitServer.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitServer.kt new file mode 100644 index 0000000..f77c787 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitServer.kt @@ -0,0 +1,55 @@ +package gg.obsidian.discordbridge.wrapper + +import gg.obsidian.discordbridge.BukkitDiscordBridge +import gg.obsidian.discordbridge.DiscordRCon +import gg.obsidian.discordbridge.command.DiscordCommandSender +import gg.obsidian.discordbridge.util.UrlAttachment +import net.md_5.bungee.api.chat.ClickEvent +import net.md_5.bungee.api.chat.ComponentBuilder +import net.md_5.bungee.api.chat.HoverEvent +import org.bukkit.Bukkit +import org.bukkit.Server +import java.util.* + +class DbBukkitServer(private val plugin: BukkitDiscordBridge, private val bukkitServer: Server) : IDbServer { + override fun broadcastAttachment(att: UrlAttachment) { + val msg = ComponentBuilder("${att.sender} sent ") + .color(net.md_5.bungee.api.ChatColor.ITALIC) + .append("an attachment") + .color(net.md_5.bungee.api.ChatColor.RESET) + .color(net.md_5.bungee.api.ChatColor.UNDERLINE) + .event(ClickEvent(ClickEvent.Action.OPEN_URL, att.url)) + .event(HoverEvent(HoverEvent.Action.SHOW_TEXT, ComponentBuilder(att.hoverText).create())) + .create() + bukkitServer.spigot().broadcast(*msg) + } + + override fun getScheduler(): IDbScheduler { + return DbBukkitScheduler(plugin, bukkitServer.scheduler) + } + + override fun getMinecraftVersion(): String { + return bukkitServer.bukkitVersion.split("-")[0] + } + + override fun getPlayer(uuid: UUID): IDbPlayer? { + return DbBukkitPlayer(bukkitServer.getOfflinePlayer(uuid)) + } + + override fun getOnlinePlayers(): List { + return bukkitServer.onlinePlayers.map { DbBukkitPlayer(it) } + } + + override fun broadcastMessage(message: String) { + bukkitServer.broadcastMessage(message) + } + + override fun dispatchCommand(sender: DiscordCommandSender, command: String) { + val rcon = DiscordRCon(sender, Bukkit.getServer().consoleSender) + bukkitServer.dispatchCommand(rcon, command) + } + + override fun getLogger(): IDbLogger { + return DbBukkitLogger(plugin.logger) + } +} diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitWorld.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitWorld.kt new file mode 100644 index 0000000..aaa80dd --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrapper/DbBukkitWorld.kt @@ -0,0 +1,9 @@ +package gg.obsidian.discordbridge.wrapper + +import org.bukkit.World + +class DbBukkitWorld(private val bukkitWorld: World) : IDbWorld { + override fun getName(): String { + return bukkitWorld.name + } +} diff --git a/src/main/resources/plugin.yml b/discordbridge-bukkit/src/main/resources/plugin.yml similarity index 91% rename from src/main/resources/plugin.yml rename to discordbridge-bukkit/src/main/resources/plugin.yml index 2e7bdbf..3f176fc 100644 --- a/src/main/resources/plugin.yml +++ b/discordbridge-bukkit/src/main/resources/plugin.yml @@ -2,13 +2,13 @@ name: DiscordBridge version: '@VERSION@' description: '@DESCRIPTION@' -authors: [Jacob Gillespie, Adam Hart] +authors: [Jacob Gillespie, DiamondIceNS] website: '@URL@' loadbefore: [SpaceBukkit, RemoteToolkitPlugin] softdepend: [Multiverse-Core] -main: gg.obsidian.discordbridge.Plugin +main: gg.obsidian.discordbridge.BukkitDiscordBridge permissions: discordbridge.discord: @@ -51,7 +51,7 @@ commands: usage: /8ball discord: description: Issue a command to the bot - usage: /discord [arguments]... + usage: [args...] f: description: Press f to pay respects usage: /f @@ -69,4 +69,4 @@ commands: usage: /roll talk: description: Talk to Cleverbot! - usage: /talk \ No newline at end of file + usage: /talk diff --git a/discordbridge-core/build.gradle b/discordbridge-core/build.gradle new file mode 100644 index 0000000..0ef03c9 --- /dev/null +++ b/discordbridge-core/build.gradle @@ -0,0 +1,21 @@ +repositories { + mavenCentral() +} + +dependencies { + compile group: 'net.dv8tion', name: 'JDA', version: '3.3.1_286' + compile group: 'org.yaml', name: 'snakeyaml', version: '1.19' + compile (group: 'org.pegdown', name:'pegdown', version: '1.6.0') { + exclude group: 'org.ow2.asm' + } + compile group: 'commons-lang', name:'commons-lang', version: '2.3' + compile group: 'com.michaelwflaherty', name: 'cleverbotapi', version: '1.0.1' + compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' + compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' +} + +shadowJar { + dependencies { + } +} +build.dependsOn(shadowJar) diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/ConfigurationNode.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/ConfigurationNode.kt new file mode 100644 index 0000000..7ed4b8c --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/ConfigurationNode.kt @@ -0,0 +1,244 @@ +// Class borrowed from dynmap-core (https://github.com/webbukkit/DynmapCore/blob/master/src/main/java/org/dynmap/ConfigurationNode.java) + +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.util.config.UserAlias +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.introspector.Property +import org.yaml.snakeyaml.nodes.* +import org.yaml.snakeyaml.reader.UnicodeReader +import org.yaml.snakeyaml.representer.Represent +import org.yaml.snakeyaml.representer.Representer +import java.io.* +import java.util.* + +class ConfigurationNode() : MutableMap { + private var entries2: MutableMap? = null + private var f: File? = null + private var yaml: Yaml? = null + + init { + entries2 = LinkedHashMap() + } + + private fun initparse() { + if(yaml == null) { + val options = DumperOptions() + + options.indent = 4 + options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK + options.isPrettyFlow = true + + val representer = EmptyNullRepresenter() + representer.addClassTag(String::class.java, Tag.STR) + representer.addClassTag(UserAlias::class.java, Tag.MAP) + + yaml = Yaml(SafeConstructor(), representer, options) + } + } + + constructor(f: File): this() { + this.f = f + } + + constructor(map: MutableMap): this() { + entries2 = map + } + + @SuppressWarnings("unchecked") + fun load(): Boolean { + initparse() + + var fis: FileInputStream? = null + try { + fis = FileInputStream(f) + val o: Any? = yaml!!.load(UnicodeReader(fis)) + if((o != null) && (o is MutableMap<*, *>)) + entries2 = o as MutableMap + fis.close() + } + catch (e: YAMLException) { + //Log.severe("Error parsing " + f.path + ". Use http://yamllint.com to debug the YAML syntax." ) + throw e + } catch(iox: IOException) { + //Log.severe("Error reading " + f.path) + return false + } finally { + if(fis != null) { + try { fis.close(); } catch (x: IOException) {} + } + } + return (entries2 != null) + } + + fun save(): Boolean { + return save(f) + } + + fun save(file: File?): Boolean { + initparse() + + var stream: FileOutputStream? = null + + file?.parentFile?.mkdirs() + + try { + stream = FileOutputStream(file) + val writer = OutputStreamWriter(stream, "UTF-8") + yaml!!.dump(entries2, writer) + writer.close() + return true + } + catch (e: IOException) { } + finally { + try { if (stream != null) stream.close() } + catch (e: IOException) { } + } + return false + } + + @SuppressWarnings("unchecked") + fun getObject(path: String): Any? { + if (path.isEmpty()) + return entries2 + val separator = path.indexOf('.') + if (separator < 0) + return get(path) + val localKey = path.substring(0, separator) + val subvalue = (get(localKey) ?: return null) as? MutableMap<*, *> ?: return null + val submap: MutableMap + try { + submap = subvalue as MutableMap + } catch (e: ClassCastException) { + return null + } + + val subpath = path.substring(separator + 1) + return ConfigurationNode(submap).getObject(subpath) + } + + fun getObject(path: String, default: Any): Any { + return getObject(path) ?: return default + } + + fun getInteger(path: String, default: Int): Int = Integer.parseInt(getObject(path, default).toString()) + fun getLong(path: String, default: Long): Double = getObject(path, default).toString().toLong().toDouble() + fun getFloat(path: String, default: Float): Float = getObject(path, default).toString().toFloat() + fun getDouble(path: String, default: Double): Double = getObject(path, default).toString().toDouble() + fun getBoolean(path: String, default: Boolean): Boolean = getObject(path, default).toString().toBoolean() + fun getString(path: String): String? = getObject(path).toString() + + fun getStrings(path: String, default: List): List { + val o = getObject(path) as? List<*> ?: return default + return o.mapTo(ArrayList()) { it.toString() } + } + + fun getString(path: String, default: String): String = getObject(path, default).toString() + + @SuppressWarnings("unchecked") + fun getList(path: String): List { + try { + return getObject(path) as List + } catch (e: ClassCastException) { + try { + val o = getObject(path) as T ?: return ArrayList() + val al = ArrayList() + al.add(o) + return al + } catch (e2: ClassCastException) { + return ArrayList() + } + } + } + + companion object { + private fun copyValue(v: Any): Any { + when (v) { + is MutableMap<*, *> -> { + //@SuppressWarnings("unchecked") + val mv = v as MutableMap + val newv = LinkedHashMap() + for(me in mv.entries) { + newv.put(me.key, copyValue(me.value)) + } + return newv + } + is List<*> -> { + @SuppressWarnings("unchecked") + val lv = v as List + return lv.indices.mapTo(ArrayList()) { copyValue(lv[it]) } + } + else -> return v + } + } + + private fun extendMap(left: MutableMap, right: MutableMap) { + val original = ConfigurationNode(left) + for(entry in right.entries) { + val key = entry.key + val value = entry.value + original.put(key, copyValue(value)) + } + } + } + + override val size: Int get() = entries2!!.size + + override fun isEmpty(): Boolean = entries2!!.isEmpty() + override fun containsKey(key: String): Boolean = entries2!!.containsKey(key) + override fun containsValue(value: Any): Boolean = entries2!!.containsValue(value) + override fun get(key: String): Any? = entries2!![key] + override fun put(key: String, value: Any): Any? = entries2!!.put(key, value) + override fun remove(key: String): Any? = entries2!!.remove(key) + + override fun putAll(from: Map) { + entries2!!.putAll(from) + } + + override fun clear() { + entries2!!.clear() + } + + override val keys: MutableSet get() = entries2!!.keys + override val values: MutableCollection get() = entries2!!.values + override val entries: MutableSet> get() = entries2!!.entries + + private class EmptyNullRepresenter : Representer() { + + init { + this.nullRepresenter = EmptyRepresentNull() + } + + private inner class EmptyRepresentNull : Represent { + override fun representData(data: Any): Node { + return representScalar(Tag.NULL, "") // Changed "null" to "" so as to avoid writing nulls + } + } + + // Code borrowed from snakeyaml (http://code.google.com/p/snakeyaml/source/browse/src/test/java/org/yaml/snakeyaml/issues/issue60/SkipBeanTest.java) + override fun representJavaBeanProperty(javaBean: Any, property: Property, propertyValue: Any, customTag: Tag): NodeTuple? { + val tuple = super.representJavaBeanProperty(javaBean, property, propertyValue, customTag) + val valueNode = tuple.valueNode + if (valueNode is CollectionNode<*>) { + // Removed null check + if (Tag.SEQ == valueNode.getTag()) { + val seq = valueNode as SequenceNode + if (seq.value.isEmpty()) { + return null // skip empty lists + } + } + if (Tag.MAP == valueNode.getTag()) { + val seq = valueNode as MappingNode + if (seq.value.isEmpty()) { + return null // skip empty maps + } + } + } + return tuple + } + // End of borrowed code + } +} diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridge.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridge.kt new file mode 100644 index 0000000..3c65953 --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridge.kt @@ -0,0 +1,466 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.command.Command +import gg.obsidian.discordbridge.command.MinecraftChatEventWrapper +import gg.obsidian.discordbridge.command.MinecraftCommandWrapper +import gg.obsidian.discordbridge.command.controller.BotControllerManager +import gg.obsidian.discordbridge.command.controller.FunCommandsController +import gg.obsidian.discordbridge.command.controller.UtilCommandsController +import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.util.enum.Cfg +import gg.obsidian.discordbridge.util.MarkdownToMinecraftSeralizer +import gg.obsidian.discordbridge.util.UrlAttachment +import gg.obsidian.discordbridge.util.config.UserAlias +import gg.obsidian.discordbridge.util.UtilFunctions.noSpace +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordChatMessage +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerDeath +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerJoin +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerLeave +import gg.obsidian.discordbridge.wrapper.IDbCommandSender +import gg.obsidian.discordbridge.wrapper.IDbLogger +import gg.obsidian.discordbridge.wrapper.IDbPlayer +import gg.obsidian.discordbridge.wrapper.IDbServer +import net.dv8tion.jda.core.OnlineStatus +import net.dv8tion.jda.core.entities.Member +import net.dv8tion.jda.core.entities.MessageChannel +import org.pegdown.PegDownProcessor +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.* +import gg.obsidian.discordbridge.util.enum.ChatColor as CC + +object DiscordBridge { + private val pegDownProc = PegDownProcessor() + private val minecraftChatControllerManager = BotControllerManager() + private val discordChatControllerManager = BotControllerManager() + private val minecraftCommandControllerManager = BotControllerManager() + private val cfgNodes: MutableMap = mutableMapOf() + private lateinit var server: IDbServer + + lateinit var logger: IDbLogger + var requests: MutableList = mutableListOf() + + // Multiverse-Core support + lateinit var mvWorlds: ConfigurationNode + var isMultiverse: Boolean = false + private set(value) { field = value } + + fun init(server: IDbServer, dataFolder: File, isMultiverse: Boolean = false) { + this.server = server + logger = this.server.getLogger() + + if (!dataFolder.exists()) dataFolder.mkdirs() + for (c in Cfg.values()) { + val filename = "${c.filename}.yml" + val file = File(dataFolder, filename) + if (!createDefaultFileFromResource("/$filename", file)) + logger.severe("Could not create default file for $filename") + val node = ConfigurationNode(file) + node.load() + cfgNodes.put(c, node) + } + server.getScheduler().runAsyncTask(Connection) + UserAliasConfig.load() + + if (isMultiverse) { + this.isMultiverse = true + mvWorlds = ConfigurationNode(File("plugins/Multiverse-Core/worlds.yml")) + mvWorlds.load().toString() + } + + minecraftChatControllerManager.registerController(FunCommandsController(), chatExclusive = true) + minecraftChatControllerManager.registerController(UtilCommandsController(), chatExclusive = true) + minecraftCommandControllerManager.registerController(FunCommandsController(), minecraftExclusive = true) + minecraftCommandControllerManager.registerController(UtilCommandsController(), minecraftExclusive = true) + discordChatControllerManager.registerController(FunCommandsController(), discordExclusive = true, chatExclusive = true) + discordChatControllerManager.registerController(UtilCommandsController(), discordExclusive = true, chatExclusive = true) + } + + fun logDebug(msg: String) { + if (!getConfig(Cfg.CONFIG).getBoolean("debug", false)) return + logger.info("[DiscordBridge] $msg") + } + + fun getServer(): IDbServer = server + fun getPegDownProcessor(): PegDownProcessor = pegDownProc + fun getConfig(type: Cfg) = cfgNodes[type]!! + + // Borrowed code from dynmap-core + /* Uses resource to create default file, if file does not yet exist */ + private fun createDefaultFileFromResource(resourcename: String, deffile: File): Boolean { + if (deffile.canRead()) return true + logger.info(deffile.path + " not found - creating default") + val inputStream = javaClass.getResourceAsStream(resourcename) + if (inputStream == null) { + logger.severe("Unable to find default resource - " + resourcename) + return false + } else { + var fos: FileOutputStream? = null + try { + fos = FileOutputStream(deffile) + while (inputStream.copyTo(fos, 512) > 0) { } + } catch (iox: IOException) { + logger.severe("ERROR creating default for " + deffile.path) + return false + } finally { + if (fos != null) + try { fos.close() } + catch (iox: IOException) { } + + try { inputStream.close() } + catch (iox: IOException) { } + + } + return true + } + } + + /*====================================== + Messaging Functions + ===================================== */ + + /** + * Sends a message to the specified Discord channel + * + * @param message the message to send + * @param channel the channel to send the message to + */ + fun sendToDiscord(message: String, channel: MessageChannel?) { + logDebug("Sending message to Discord - $message") + Connection.send(message, channel) + } + + /** + * Broadcast a message on the Minecraft server + * + * @param message the message to send + */ + fun sendToMinecraft(message: String) { + server.broadcastMessage(message) + } + + fun sendToMinecraft(att: UrlAttachment) { + server.broadcastAttachment(att) + } + + + fun reload(callback: Runnable) { + for (cfg in cfgNodes.values) cfg.load() + if (isMultiverse) mvWorlds.load() + UserAliasConfig.load() + Connection.reconnect(callback) + } + + /** + * @return a list of names of all players currently on the Minecraft server + */ + fun getOnlinePlayers(): List { + val names: MutableList = mutableListOf() + val players = server.getOnlinePlayers().toTypedArray() + players.mapTo(names) { it.getName() } + return names.toList() + } + + /** + * Opens an alias link request and sends it to the target Discord user + * + * @param player the Minecraft player that initiated the request + * @param discriminator the Discord username+discriminator of the target Discord user + * @return a Discord Member object, or null if no matching member was found + */ + fun registerUserRequest(player: IDbPlayer, discriminator: String): Member? { + val users = Connection.listUsers() + val found: Member = users.find { it.user.name + "#" + it.user.discriminator == discriminator } ?: return null + + val ua = UserAlias(player.getUUID().toString(), found.user.id) + requests.add(ua) + val msg = "Minecraft user '${player.getName()}' has requested to become associated with your Discord" + + " account. If this is you, respond '${Connection.JDA.selfUser.asMention} confirm'. If this is not" + + " you, respond ${Connection.JDA.selfUser.asMention} deny'." + val member = Connection.JDA.getUserById(ua.discordId) + member.openPrivateChannel().queue({p -> p.sendMessage(msg).queue()}) + return found + } + + /** + * @return a formatted string listing the Discord IDs of all Discord users in the relay channel + */ + fun getDiscordMembersAll(): String { + val users = Connection.listUsers() + + if (users.isEmpty()) + return "${CC.YELLOW}No Discord members could be found. Either server is empty or an error has occurred." + + var response = "${CC.YELLOW}Discord users:" + for (user in users) { + response += if (user.user.isBot) "\n${CC.GOLD}- ${user.effectiveName} (Bot) | ${user.user.name}#${user.user.discriminator}${CC.RESET}" + else "\n${CC.YELLOW}- ${user.effectiveName} | ${user.user.name}#${user.user.discriminator}${CC.RESET}" + } + return response.trim() + } + + /** + * @return a formatted string listing all Discord users in the relay channel who are online along with their statuses + */ + fun getDiscordMembersOnline(): String { + val onlineUsers = Connection.listOnline() + if (onlineUsers.isEmpty()) + return "${CC.YELLOW}No Discord members could be found. Either server is empty or an error has occurred." + + var response = "" + if (onlineUsers.any { it.onlineStatus == OnlineStatus.ONLINE }) { + response += "\n${CC.DARK_GREEN}Online:${CC.RESET}" + for (user in onlineUsers.filter { it.onlineStatus == OnlineStatus.ONLINE }) { + response += if (user.user.isBot) "\n${CC.DARK_GREEN}- ${user.effectiveName} (Bot)${CC.RESET}" + else "\n${CC.DARK_GREEN}- ${user.effectiveName}${CC.RESET}" + } + } + if (onlineUsers.any { it.onlineStatus == OnlineStatus.IDLE }) { + response += "\n${CC.YELLOW}Idle:${CC.RESET}" + for (user in onlineUsers.filter { it.onlineStatus == OnlineStatus.IDLE }) { + response += if (user.user.isBot) "\n${CC.YELLOW}- ${user.effectiveName} (Bot)${CC.RESET}" + else "\n${CC.YELLOW}- ${user.effectiveName}${CC.RESET}" + } + } + if (onlineUsers.any { it.onlineStatus == OnlineStatus.DO_NOT_DISTURB }) { + response += "\n${CC.RED}Do Not Disturb:${CC.RESET}" + for (user in onlineUsers.filter { it.onlineStatus == OnlineStatus.DO_NOT_DISTURB }) { + response += if (user.user.isBot) "\n${CC.RED}- ${user.effectiveName} (Bot)${CC.RESET}" + else "\n${CC.RED}- ${user.effectiveName}${CC.RESET}" + } + } + + response.replaceFirst("\n", "") + return response.trim() + } + + /*====================================== + Message Formatting Functions + ===================================== */ + + /** + * Attempts to convert all instances of "@name" into Discord @tag mentions + * + * This should work for "@", "@" (if an alias is linked), + * and "@" + * + * NOTE: If the Discord name contains spaces, that name must be typed in this string without spaces. + * e.g. a member named "Discord Bridge" must be tagged as "@DiscordBridge" + * + * @param message the message to format + * @return the formatted message + */ + fun convertAtMentions(message: String): String { + var newMessage = message + + val discordusers = Connection.listUsers() + val discordaliases: MutableList> = mutableListOf() + + for (du in discordusers) + for ((mcUuid, discordId) in UserAliasConfig.aliases) + if (discordId == du.user.id) { + val player = server.getPlayer(UUID.fromString(mcUuid)) + if (player != null) discordaliases.add(Pair(player.getName(), du)) + } + + for (match in Regex("""(?:^| )@(\w+)""").findAll(message)) { + val found: Member? = discordusers.firstOrNull { + it.user.name.noSpace().toLowerCase() == match.groupValues[1].toLowerCase() || + it.user.name + "#" + it.user.discriminator == match.groupValues[1].toLowerCase() || + it.effectiveName.noSpace().toLowerCase() == match.groupValues[1].toLowerCase() + } + if (found != null) newMessage = newMessage.replaceFirst("@${match.groupValues[1]}", found.asMention) + + val found2: Pair? = discordaliases.firstOrNull { + it.first.toLowerCase() == match.groupValues[1].toLowerCase() + } + if (found2 != null) newMessage = newMessage.replaceFirst("@${match.groupValues[1]}", found2.second.asMention) + } + + return newMessage + } + + /** + * Attempts to de-convert all instances of Discord @tag mentions back into simple "@name" syntax + * + * @param message the message to format + * @return the formatted message + */ + fun deconvertAtMentions(message: String): String { + var modifiedMessage = message + for (match in Regex("""<@!(\d+)>|<@(\d+)>""").findAll(message)) { + val discordUser = Connection.listUsers().firstOrNull { it.user.id == match.groupValues[1] || it.user.id == match.groupValues[2] } + if (discordUser != null) modifiedMessage = modifiedMessage.replace(match.value, "@"+discordUser.effectiveName) + } + return modifiedMessage + } + + /** + * Scans the input string for occurrences of Minecraft names in the alias registry and replaces them with + * their corresponding Discord aliases + * + * @param message the message to format + * @return the formatted message + */ + fun translateAliasesToDiscord(message: String): String { + var modifiedMessage = message + for ((mcUuid, discordId) in UserAliasConfig.aliases) { + val player = server.getPlayer(UUID.fromString(mcUuid)) + if (player != null) { + val nameMC = player.getName() + val discordUser = Connection.listUsers().firstOrNull { it.user.id == discordId } + val nameDis = if (discordUser != null) discordUser.effectiveName else Connection.JDA.getUserById(discordId).name + modifiedMessage = modifiedMessage.replace(nameMC, nameDis) + } + } + return modifiedMessage + } + + /** + * Scans the input string for occurrences of Discord names in the alias registry and replaces them with + * their corresponding Minecraft aliases + * + * @param message the message to format + * @return the formatted message + */ + fun translateAliasesToMinecraft(message: String): String { + var modifiedMessage = message + for ((mcUuid, discordId) in UserAliasConfig.aliases) { + val player = server.getPlayer(UUID.fromString(mcUuid)) + if (player != null) { + val nameMC = player.getName() + val nameDis = Connection.JDA.getUserById(discordId).name + modifiedMessage = modifiedMessage.replace(nameDis, nameMC) + val discordUser = Connection.listUsers().firstOrNull { it.user.id == discordId } + if (discordUser != null) modifiedMessage = modifiedMessage.replace(discordUser.effectiveName, nameMC) + } + } + return modifiedMessage + } + + fun handleServerStart() { + Connection.onServerReady() + } + + fun handleServerStop() { + if (getConfig(Cfg.CONFIG).getBoolean("announce-server-start-stop", true)) + sendToDiscord(getConfig(Cfg.CONFIG).getString("templates.discord.server-stop", "Shutting down..."), Connection.getRelayChannel()) + Connection.disconnect() + } + + fun handlePlayerChat(player: IDbPlayer, message: String, isCancelled: Boolean): String { + // TODO: the order of these if statements may produce undesired behavior + logDebug("Received a chat event from ${player.getName()}: $message") + if (!getConfig(Cfg.CONFIG).getBoolean("player-messages.chat", true)) return message + if (isCancelled && !getConfig(Cfg.CONFIG).getBoolean("relay-cancelled-messages", true)) return message + if (player.isVanished() && !getConfig(Cfg.CONFIG).getBoolean("if-vanished.player-chat", false)) return message + + // Emoticons! + val newMessage = message.replace(":lenny:", "( \u0361\u00B0 \u035C\u0296 \u0361\u00B0)") + .replace(":tableflip:", "(\u256F\u00B0\u25A1\u00B0\uFF09\u256F\uFE35 \u253B\u2501\u253B") + .replace(":unflip:", "\u252C\u2500\u2500\u252C \u30CE( \u309C-\u309C\u30CE)") + .replace(":shrug:", "\u00AF\\_(\u30C4)_/\u00AF") + .replace(":donger:", "\u30FD\u0F3C\u0E88\u0644\u035C\u0E88\u0F3D\uFF89") + .replace(":disapproval:", "\u0CA0_\u0CA0") + .replace(":kawaii:", "(\uFF89\u25D5\u30EE\u25D5)\uFF89*:\uFF65\uFF9F\u2727") + .replace(":amendo:", "\u0F3C \u3064 \u25D5_\u25D5 \u0F3D\u3064") + .replace(":yuno:", "\u10DA(\u0CA0\u76CA\u0CA0\u10DA)") + .replace(":fingerguns:", "(\u261E\uFF9F\u30EE\uFF9F)\u261E") + .replace(":fingergunsr:", "(\u261E\uFF9F\u30EE\uFF9F)\u261E") + .replace(":fingergunsl:", "\u261C(\uFF9F\u30EE\uFF9F\u261C)") + .replace(":fight:", "(\u0E07 \u2022\u0300_\u2022\u0301)\u0E07") + .replace(":happygary:", "\u1555(\u141B)\u1557") + .replace(":denko:", "(\u00B4\uFF65\u03C9\uFF65`)") + .replace(":masteryourdonger:", "(\u0E07 \u0360\u00B0 \u0644\u035C \u00B0)\u0E07") + + //val wrapper = MinecraftChatEventWrapper(AsyncPlayerChatEvent(true, event.player, event.message, event.recipients)) + server.getScheduler().runAsyncTask(Runnable { minecraftChatControllerManager.dispatchMessage(MinecraftChatEventWrapper(player, newMessage)) }) + + return MarkdownToMinecraftSeralizer().toMinecraft(pegDownProc.parseMarkdown(newMessage.toCharArray())) + } + + fun handlePlayerJoin(player: IDbPlayer) { + val username = player.getName() + var worldname = player.getWorld().getName() + logDebug("Received a join event for $username") + if (!getConfig(Cfg.CONFIG).getBoolean("messages.player-join", true)) return + if (player.isVanished() && !getConfig(Cfg.CONFIG).getBoolean("if-vanished.player-join", false)) return + + // Get world alias if Multiverse is installed + if (DiscordBridge.isMultiverse) { + val obj = DiscordBridge.mvWorlds.getObject("worlds.$worldname") + if (obj != null) { + val alias = (obj as Map<*, *>)["alias"] + if (alias is String && alias.isNotEmpty()) worldname = alias + } + else + DiscordBridge.logger.warning("Could not fetch world alias from config " + + "(did you `/discord reload` yet?)") + } + + var formattedMessage = player.toDiscordPlayerJoin(worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handlePlayerQuit(player: IDbPlayer) { + val username = player.getName() + var worldname = player.getWorld().getName() + logDebug("Received a leave event for $username") + if (!getConfig(Cfg.CONFIG).getBoolean("messages.player-leave", true)) return + if (player.isVanished() && !getConfig(Cfg.CONFIG).getBoolean("if-vanished.player-leave", false)) return + + // Get world alias if Multiverse is installed + if (DiscordBridge.isMultiverse) { + val obj = DiscordBridge.mvWorlds.getObject("worlds.$worldname") + if (obj != null) { + val alias = (obj as Map<*, *>)["alias"] + if (alias is String && alias.isNotEmpty()) worldname = alias + } + else + DiscordBridge.logger.warning("Could not fetch world alias from config " + + "(did you `/discord reload` yet?)") + } + + var formattedMessage = player.toDiscordPlayerLeave(worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handlePlayerDeath(player: IDbPlayer, deathMessage: String) { + val username = player.getName() + var worldname = player.getWorld().getName() + + if (!getConfig(Cfg.CONFIG).getBoolean("messages.player-death", false)) return + if (player.isVanished() && !getConfig(Cfg.CONFIG).getBoolean("if-vanished.player-death", false)) return + + // Get world alias if Multiverse is installed + if (DiscordBridge.isMultiverse) { + val obj = DiscordBridge.mvWorlds.getObject("worlds.$worldname") + if (obj != null) { + val alias = (obj as Map<*, *>)["alias"] + if (alias is String && alias.isNotEmpty()) worldname = alias + } + else + DiscordBridge.logger.warning("Could not fetch world alias from config " + + "(did you `/discord reload` yet?)") + } + + var formattedMessage = deathMessage.toDiscordPlayerDeath(username, worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handleDynmapChat(name: String, message: String) { + sendToDiscord(translateAliasesToDiscord(message.toDiscordChatMessage(name, "Dynmap")), Connection.getRelayChannel()) + } + + fun handleCommand(sender: IDbCommandSender, commandName: String, args: Array): Boolean { + return minecraftCommandControllerManager.dispatchMessage(MinecraftCommandWrapper(sender, commandName, args)) + } + + fun getServerCommands(): List { + return minecraftCommandControllerManager.getCommands().values.toList() + } +} diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/UserAliasConfig.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/UserAliasConfig.kt new file mode 100644 index 0000000..e5aad0c --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/UserAliasConfig.kt @@ -0,0 +1,43 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.util.enum.Cfg +import gg.obsidian.discordbridge.util.config.UserAlias + +/** + * An accessor object for the users config file + */ +object UserAliasConfig { + var aliases: List = mutableListOf() + + /** + * Load the stored aliases from file into memory + */ + fun load() { + val list = DiscordBridge.getConfig(Cfg.ALIAS).getList>("aliases") + aliases = list.castTo({ UserAlias(it) }) + } + + /** + * Adds a new alias to the list and saves the updated list to file + */ + fun add(ua: UserAlias) { + aliases = aliases.plus(ua) + DiscordBridge.getConfig(Cfg.ALIAS).put("aliases", aliases) + DiscordBridge.getConfig(Cfg.ALIAS).save() + DiscordBridge.getConfig(Cfg.ALIAS).load() + } + + /** + * Removes an alias from the list and saves the updated list to file + */ + fun remove(ua: UserAlias) { + aliases = aliases.minus(ua) + DiscordBridge.getConfig(Cfg.ALIAS).put("aliases", aliases) + DiscordBridge.getConfig(Cfg.ALIAS).save() + DiscordBridge.getConfig(Cfg.ALIAS).load() + } + + private inline fun List>.castTo(factory: (Map) -> T): List { + return this.mapTo(mutableListOf()) { factory(it) }.toList() + } +} diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/Command.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/Command.kt new file mode 100644 index 0000000..a7689c5 --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/Command.kt @@ -0,0 +1,32 @@ +package gg.obsidian.discordbridge.command + +import java.lang.reflect.Method + +/** + * Represents a command that the bot can execute + * + * @param name the name of the command (not necessarily the name of the method called) + * @param usage the usage description of the method + * @param description a description of the command's function + * @param parameters a List of the command arguments' expected Java class types + * @param relayTriggerMessage whether the message that triggered this command should be relayed + * @param squishExcessArgs whether this command should combine excess arguments into one + * @param isTagged if the output of this command should be prepended with "@invokerName | " + * @param isPrivate if the output of this command should be sent via DM/PM to the invoker + * @param controllerClass the Java class type of the IBotController that defines this command + * @param commandMethod the method that is called when this command is invoked + */ +data class Command( + val aliases: Array, + val usage: String, + val description: String, + val help: String, + val parameters: List>, + val relayTriggerMessage: Boolean, + val squishExcessArgs: Boolean, + val ignoreExcessArgs: Boolean, + val isTagged: Boolean, + val isPrivate: Boolean, + val controllerClass: Class<*>, + val commandMethod: Method +) \ No newline at end of file diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordCommandSender.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordCommandSender.kt new file mode 100644 index 0000000..ae74ca4 --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordCommandSender.kt @@ -0,0 +1,14 @@ +package gg.obsidian.discordbridge.command + +import net.dv8tion.jda.core.entities.MessageChannel + +class DiscordCommandSender(val senderName: String, private val channel: MessageChannel) { + fun sendMessage(message: String) { + channel.sendMessage(message) + } + + fun sendMessage(messages: Array?) { + if (messages != null) + for (m in messages) channel.sendMessage(m) + } +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordMessageWrapper.kt similarity index 79% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordMessageWrapper.kt index c00ff01..7b1be10 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/DiscordMessageWrapper.kt @@ -1,6 +1,7 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge.command -import gg.obsidian.discordbridge.Config +import gg.obsidian.discordbridge.DiscordBridge +import gg.obsidian.discordbridge.util.enum.Cfg import net.dv8tion.jda.core.entities.ChannelType import net.dv8tion.jda.core.entities.Message import net.dv8tion.jda.core.entities.MessageChannel @@ -10,8 +11,7 @@ import net.dv8tion.jda.core.entities.MessageChannel * * @param originalMessage the underlying Message instance */ -class MessageWrapper(val originalMessage: Message) : IEventWrapper { - +class DiscordMessageWrapper(val originalMessage: Message) : IEventWrapper { /** * Returns a formatted mention tag in the form <@##########> */ @@ -33,9 +33,9 @@ class MessageWrapper(val originalMessage: Message) : IEventWrapper { */ override val isFromRelayChannel: Boolean get() = if (originalMessage.isFromType(ChannelType.PRIVATE)) false - else originalMessage.guild.id == Config.SERVER_ID + else originalMessage.guild.id == DiscordBridge.getConfig(Cfg.CONFIG).getString("server-id") && originalMessage.isFromType(ChannelType.TEXT) - && originalMessage.textChannel.name.equals(Config.CHANNEL, true) + && originalMessage.textChannel.name.equals(DiscordBridge.getConfig(Cfg.CONFIG).getString("channel"), true) /** * The message of this event @@ -64,5 +64,4 @@ class MessageWrapper(val originalMessage: Message) : IEventWrapper { */ override val senderId: String get() = originalMessage.author.id - } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/IEventWrapper.kt similarity index 71% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/IEventWrapper.kt index 5116f7c..8ef4d04 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/IEventWrapper.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge.command import net.dv8tion.jda.core.entities.MessageChannel @@ -13,7 +13,7 @@ interface IEventWrapper { /** * The message in the event * - * For MessageWrapper instances, this calls getContent() + * For DiscordMessageWrapper instances, this calls getContent() * * Otherwise, this is identical to rawMessage */ @@ -21,7 +21,7 @@ interface IEventWrapper { /** * The raw message in the event * - * For MessageWrapper instances, this calls getRawContent() + * For DiscordMessageWrapper instances, this calls getRawContent() * * Otherwise, this is identical to message */ @@ -29,7 +29,7 @@ interface IEventWrapper { /** * The name of the author of the event in @tag format * - * For MessageWrapper instances, this will return a mention tag in the form <@##########> + * For DiscordMessageWrapper instances, this will return a mention tag in the form <@##########> * * Otherwise, this will return the player's username prefixed with '@' */ @@ -37,7 +37,7 @@ interface IEventWrapper { /** * The originating channel of the message * - * For MessageWrapper instances, this returns the origin channel or private channel of the message + * For DiscordMessageWrapper instances, this returns the origin channel or private channel of the message * * Otherwise, this returns Connection.getRelayChannel() * @see gg.obsidian.discordbridge.discord.Connection.getRelayChannel @@ -46,7 +46,7 @@ interface IEventWrapper { /** * The ID string of the message author * - * For MessageWrapper instances, this returns the author's Discord ID + * For DiscordMessageWrapper instances, this returns the author's Discord ID * * Otherwise, this returns the author's Minecraft UUID */ @@ -54,7 +54,7 @@ interface IEventWrapper { /** * Whether this message is from the channel that is relayed to Minecraft * - * For MessageWrapper instances, this is true if the inner event's getChannel() is equal to + * For DiscordMessageWrapper instances, this is true if the inner event's getChannel() is equal to * Connection.getRelayChannel(), and false otherwise * @see gg.obsidian.discordbridge.discord.Connection.getRelayChannel * diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftChatEventWrapper.kt similarity index 65% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftChatEventWrapper.kt index 9166b38..88fdd59 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftChatEventWrapper.kt @@ -1,37 +1,38 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge.command import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.wrapper.IDbPlayer import net.dv8tion.jda.core.entities.MessageChannel -import org.bukkit.event.player.AsyncPlayerChatEvent /** - * A wrapper for Bukkit's AsyncPlayerChatEvent class + * Event wrapper for a player chat event * - * @param event the underlying AsyncPlayerChatEvent instance + * @param player the player who triggered the event + * @param chatMessage the contents of the message that was sent */ -class AsyncPlayerChatEventWrapper(val event: AsyncPlayerChatEvent) : IEventWrapper { +class MinecraftChatEventWrapper(val player: IDbPlayer, private val chatMessage: String) : IEventWrapper { /** * The Minecraft username of the event author */ override val senderName: String - get() = event.player.name + get() = player.getName() /** * The message of this event */ override val message: String - get() = event.message + get() = chatMessage /** * The raw message of this event * * This is identical to the message property for this wrapper type */ override val rawMessage: String - get() = event.message + get() = chatMessage /** * The Minecraft username of the sender in "@name" format */ override val senderAsMention: String - get() = "@" + event.player.name + get() = "@" + player.getName() /** * Returns the value at Connection.getRelayChannel() * @see Connection.getRelayChannel @@ -42,11 +43,10 @@ class AsyncPlayerChatEventWrapper(val event: AsyncPlayerChatEvent) : IEventWrapp * The message author's Minecraft UUID */ override val senderId: String - get() = event.player.uniqueId.toString() + get() = player.getUUID().toString() /** * Always returns true for this wrapper type */ override val isFromRelayChannel: Boolean get() = true - } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftCommandWrapper.kt similarity index 77% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftCommandWrapper.kt index 141817b..a7260e0 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/MinecraftCommandWrapper.kt @@ -1,19 +1,18 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge.command import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.wrapper.IDbCommandSender +import gg.obsidian.discordbridge.wrapper.IDbPlayer import net.dv8tion.jda.core.entities.MessageChannel -import org.bukkit.command.Command -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player /** * A wrapper for the parameters passed to onCommand() in Bukkit's CommandExecutor class * * @param sender the sender of the command - * @param command the command that was invoked + * @param commandName the name of the command that was invoked * @param args an array of argument strings passed to the command */ -class MinecraftCommandWrapper(val sender: CommandSender, val command: Command, +class MinecraftCommandWrapper(val sender: IDbCommandSender, val commandName: String, val args: Array) : IEventWrapper { /** * The Minecraft username of the command sender @@ -21,7 +20,7 @@ class MinecraftCommandWrapper(val sender: CommandSender, val command: Command, * Returns "Console" if the command was sent from the server console */ override val senderName: String - get() = if (sender is Player) sender.name else "Console" + get() = (sender as? IDbPlayer)?.getName() ?: "Console" /** * Returns a space-delimited string of all the arguments passed with the command * @@ -40,7 +39,7 @@ class MinecraftCommandWrapper(val sender: CommandSender, val command: Command, * The Minecraft username of the command sender in "@name" format */ override val senderAsMention: String - get() = "@${sender.name}" + get() = "@${sender.getName()}" /** * Returns the value at Connection.getRelayChannel() * @see Connection.getRelayChannel @@ -51,11 +50,10 @@ class MinecraftCommandWrapper(val sender: CommandSender, val command: Command, * The command sender's Minecraft UUID */ override val senderId: String - get() = (sender as? Player)?.uniqueId?.toString() ?: "" + get() = (sender as? IDbPlayer)?.getUUID()?.toString() ?: "" /** * Always returns true for this wrapper type */ override val isFromRelayChannel: Boolean get() = true - } diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/BotCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/BotCommand.kt new file mode 100644 index 0000000..86947e4 --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/BotCommand.kt @@ -0,0 +1,22 @@ +package gg.obsidian.discordbridge.command.annotation + +/** + * Annotates a function as a command the bot can run. + * + * @param aliases an optional field to override the command's access name if it is not the same as the method name + * @param usage a short string that describes the command's parameter syntax + * @param desc a short string that describes the command's function + * @param relayTriggerMessage whether the message used to trigger this command should be relayed + * @param squishExcessArgs if true, this command will put all extra args passed to it into a single string + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class BotCommand( + val aliases: Array, + val desc: String, + val usage: String = "", + val help: String = "", + val relayTriggerMessage: Boolean = true, + val squishExcessArgs: Boolean = false, + val ignoreExcessArgs: Boolean = false +) diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/ChatExclusiveCommand.kt similarity index 80% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/ChatExclusiveCommand.kt index 3b6a510..66d16a1 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/ChatExclusiveCommand.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands.annotations +package gg.obsidian.discordbridge.command.annotation /** * Annotates a BotCommand as a command that is exposed to Discord chat and Minecraft chat diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/DiscordExclusiveCommand.kt similarity index 80% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/DiscordExclusiveCommand.kt index f9e8f1a..1fc4e11 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/DiscordExclusiveCommand.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands.annotations +package gg.obsidian.discordbridge.command.annotation /** * Annotates a BotCommand as a command that is only exposed to Discord chat diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/MinecraftExclusiveCommand.kt similarity index 81% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/MinecraftExclusiveCommand.kt index 9611d52..adbc00b 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/MinecraftExclusiveCommand.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands.annotations +package gg.obsidian.discordbridge.command.annotation /** * Annotates a BotCommand as a command that is only exposed as a Minecraft console command diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/PrivateResponse.kt similarity index 84% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/PrivateResponse.kt index fe5d10d..4c42618 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/PrivateResponse.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands.annotations +package gg.obsidian.discordbridge.command.annotation /** * Annotates a BotCommand as a command that will return its output privately to the invoker using @@ -14,4 +14,4 @@ package gg.obsidian.discordbridge.commands.annotations */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class PrivateResponse \ No newline at end of file +annotation class PrivateResponse diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/TaggedResponse.kt similarity index 78% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/TaggedResponse.kt index ebcb073..22f8a7e 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/annotation/TaggedResponse.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands.annotations +package gg.obsidian.discordbridge.command.annotation /** * Annotates a BotCommand as a command where the response will be prepended with "@invokerName | " @@ -9,4 +9,4 @@ package gg.obsidian.discordbridge.commands.annotations */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class TaggedResponse \ No newline at end of file +annotation class TaggedResponse diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/controller/BotControllerManager.kt similarity index 57% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/controller/BotControllerManager.kt index 8f09c83..b265e91 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/command/controller/BotControllerManager.kt @@ -1,33 +1,28 @@ -package gg.obsidian.discordbridge.commands.controllers +package gg.obsidian.discordbridge.command.controller -import gg.obsidian.discordbridge.Config -import gg.obsidian.discordbridge.Plugin -import gg.obsidian.discordbridge.commands.* -import gg.obsidian.discordbridge.commands.annotations.* +import gg.obsidian.discordbridge.DiscordBridge +import gg.obsidian.discordbridge.command.* +import gg.obsidian.discordbridge.command.annotation.* import gg.obsidian.discordbridge.discord.Connection -import gg.obsidian.discordbridge.utils.MarkdownToMinecraftSeralizer -import gg.obsidian.discordbridge.utils.Script -import gg.obsidian.discordbridge.utils.UtilFunctions.noSpace -import gg.obsidian.discordbridge.utils.UtilFunctions.stripColor -import gg.obsidian.discordbridge.utils.UtilFunctions.toDiscordChatMessage -import gg.obsidian.discordbridge.utils.UtilFunctions.toMinecraftChatMessage +import gg.obsidian.discordbridge.util.enum.Cfg +import gg.obsidian.discordbridge.util.MarkdownToMinecraftSeralizer +import gg.obsidian.discordbridge.util.config.Script +import gg.obsidian.discordbridge.util.UtilFunctions.noSpace +import gg.obsidian.discordbridge.util.UtilFunctions.stripColor +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordChatMessage +import gg.obsidian.discordbridge.util.UtilFunctions.toMinecraftChatMessage import net.dv8tion.jda.core.Permission -import org.bukkit.Bukkit import java.lang.reflect.Method import java.util.* -import java.util.logging.Level -import gg.obsidian.discordbridge.Config as cfg -import org.bukkit.ChatColor as CC +import gg.obsidian.discordbridge.util.enum.ChatColor as CC /** * A class that manages an assortment of IBotControllers and allows dynamic access to a configurable assortment * of their commands - * - * @param plugin a reference to the base Plugin object + * * @see IBotController */ -class BotControllerManager(val plugin: Plugin) { - +class BotControllerManager { private val commands: MutableMap = mutableMapOf() private val controllers: MutableMap, IBotController> = mutableMapOf() @@ -69,7 +64,7 @@ class BotControllerManager(val plugin: Plugin) { * @param annotation the BotCommand annotation object of the method */ private fun registerControllerMethod(controllerClass: Class<*>, method: Method, annotation: BotCommand) { - val commandName = if (annotation.name.isEmpty()) method.name.toLowerCase() else annotation.name + val commandAliases = annotation.aliases val usage = annotation.usage val methodParameters = method.parameters @@ -79,9 +74,13 @@ class BotControllerManager(val plugin: Plugin) { val parameters = (1 until methodParameters.size).mapTo(ArrayList>()) { methodParameters[it].type } val isTagged: Boolean = method.getAnnotation(TaggedResponse::class.java) != null val isPrivate: Boolean = method.getAnnotation(PrivateResponse::class.java) != null - val command = Command(commandName, usage, annotation.description, parameters, annotation.relayTriggerMessage, - annotation.squishExcessArgs, isTagged, isPrivate, controllerClass, method) - commands.put(command.name, command) + val command = Command(commandAliases, usage, annotation.desc, annotation.help, parameters, annotation.relayTriggerMessage, + annotation.squishExcessArgs, annotation.ignoreExcessArgs, isTagged, isPrivate, controllerClass, method) + commands.put(command.aliases[0], command) + } + + fun getCommands(): Map { + return commands } // ============================================= @@ -97,11 +96,7 @@ class BotControllerManager(val plugin: Plugin) { // Short circuit if event was a Minecraft command if (event is MinecraftCommandWrapper) { - val command = commands[event.command.name] - if (command == null) { - commandNotFound(event, event.command.name) - return true - } + val command = commands[event.commandName] ?: return true return invokeBotCommand(command, controllers, event, event.args.asList().toTypedArray()) } @@ -109,19 +104,19 @@ class BotControllerManager(val plugin: Plugin) { if (sendScriptedResponse(event)) return true // command - if (Config.COMMAND_PREFIX.isNotBlank() && event.rawMessage.startsWith(Config.COMMAND_PREFIX)) { - val split = event.rawMessage.replaceFirst(Config.COMMAND_PREFIX, "").trim().split("\\s+".toRegex()).toTypedArray() + if (DiscordBridge.getConfig(Cfg.CONFIG).getString("command-prefix", "").isNotBlank() && event.rawMessage.startsWith(DiscordBridge.getConfig(Cfg.CONFIG).getString("command-prefix", ""))) { + val split = event.rawMessage.replaceFirst(DiscordBridge.getConfig(Cfg.CONFIG).getString("command-prefix", ""), "").trim().split("\\s+".toRegex()).toTypedArray() return parseCommand(event, split, false) } // @ command from Minecraft - if (event is AsyncPlayerChatEventWrapper && event.rawMessage.startsWith("@${Config.USERNAME.noSpace()} ")) { - val split = event.rawMessage.replaceFirst("@${Config.USERNAME.noSpace()} ", "").trim().split("\\s+".toRegex()).toTypedArray() + if (event is MinecraftChatEventWrapper && event.rawMessage.startsWith("@${DiscordBridge.getConfig(Cfg.CONFIG).getString("username", "DiscordBridge").noSpace()} ")) { + val split = event.rawMessage.replaceFirst("@${DiscordBridge.getConfig(Cfg.CONFIG).getString("username", "DiscordBridge").noSpace()} ", "").trim().split("\\s+".toRegex()).toTypedArray() return parseCommand(event, split, true) } // @ command from Discord - if (event is MessageWrapper && event.rawMessage.startsWith(Connection.JDA.selfUser.asMention + " ")) { + if (event is DiscordMessageWrapper && event.rawMessage.startsWith(Connection.JDA.selfUser.asMention + " ")) { val split = event.rawMessage.replaceFirst(Connection.JDA.selfUser.asMention + " ", "").trim().split("\\s+".toRegex()).toTypedArray() return parseCommand(event, split, true) } @@ -138,7 +133,7 @@ class BotControllerManager(val plugin: Plugin) { * @return true if a trigger was found and successfully responded to, false otherwise */ private fun sendScriptedResponse(event: IEventWrapper): Boolean { - val responses = plugin.script.data.getList("responses").checkItemsAre