From 3d2628628e1d908947e7038715a54ff5fc0fad1a Mon Sep 17 00:00:00 2001 From: DiamondIceNS Date: Sat, 30 Dec 2017 18:44:16 -0600 Subject: [PATCH 01/30] Break up project into core and wrapper modules discordbridge-core: the core API module discordbridge-bukkit: bukkit plugin wrapper Project will build and plugin will load, but many things are still very, very broken. Discord relaying works flawlessly. All relevant server events are captured and relayed. Plugin can create and read configuration files, but keels over and dies when attempting to write and read back user-defined objects. Discord alias linking is useless without this feature. Some commands like 8ball seem to be broken. This seems to be an issue that affects the current master branch as well. --- build.gradle | 107 ++-- discordbridge-bukkit/build.gradle | 48 ++ .../discordbridge/DiscordBridgePlugin.kt | 35 ++ .../discordbridge}/DiscordCommandSender.kt | 2 +- .../obsidian/discordbridge/EventListener.kt | 54 ++ .../discordbridge/wrappers/Command.kt | 10 + .../discordbridge/wrappers/ConsoleSender.kt | 26 + .../obsidian/discordbridge/wrappers/Player.kt | 31 ++ .../discordbridge/wrappers/Scheduler.kt | 12 + .../obsidian/discordbridge/wrappers/Server.kt | 52 ++ .../obsidian/discordbridge/wrappers/World.kt | 10 + .../src}/main/resources/plugin.yml | 4 +- discordbridge-core/build.gradle | 29 ++ .../discordbridge/ConfigurationNode.kt | 413 ++++++++++++++++ .../obsidian/discordbridge/DiscordBridge.kt | 465 ++++++++++++++++++ .../obsidian/discordbridge/UserAliasConfig.kt | 43 ++ .../commands/DiscordCommandSender.kt | 84 ++++ .../commands/DiscordMessageWrapper.kt | 8 +- .../discordbridge/commands/IEventWrapper.kt | 12 +- .../commands/MinecraftChatEventWrapper.kt | 14 +- .../commands/MinecraftCommandWrapper.kt | 14 +- .../commands/annotations/BotCommand.kt | 0 .../annotations/ChatExclusiveCommand.kt | 0 .../annotations/DiscordExclusiveCommand.kt | 0 .../annotations/MinecraftExclusiveCommand.kt | 0 .../commands/annotations/PrivateResponse.kt | 0 .../commands/annotations/TaggedResponse.kt | 0 .../controllers/BotControllerManager.kt | 273 +++++----- .../controllers/FunCommandsController.kt | 57 ++- .../commands/controllers}/IBotController.kt | 2 +- .../controllers/UtilCommandsController.kt | 95 ++-- .../discordbridge/discord/Connection.kt | 27 +- .../discordbridge/discord/Listener.kt | 20 +- .../obsidian/discordbridge/util/ChatColor.kt | 152 ++++++ .../util}/MarkdownToMinecraftSeralizer.kt | 2 +- .../gg/obsidian/discordbridge/util/Rating.kt | 28 ++ .../obsidian/discordbridge/util}/Respect.kt | 12 +- .../gg/obsidian/discordbridge/util}/Script.kt | 21 +- .../obsidian/discordbridge/util/UserAlias.kt | 20 + .../discordbridge/util}/UtilFunctions.kt | 29 +- .../discordbridge/wrappers/ICommand.kt | 5 + .../discordbridge/wrappers/ICommandSender.kt | 10 + .../discordbridge/wrappers/IConsoleSender.kt | 4 + .../discordbridge/wrappers/IPlayer.kt | 6 + .../discordbridge/wrappers/IScheduler.kt | 5 + .../discordbridge/wrappers/IServer.kt | 29 ++ .../obsidian/discordbridge/wrappers/IWorld.kt | 5 + .../src}/main/resources/8ball.yml | 0 .../src}/main/resources/config.yml | 0 .../src}/main/resources/f.yml | 30 +- .../src}/main/resources/insult.yml | 0 .../src}/main/resources/rate.yml | 96 ++-- .../src}/main/resources/script.yml | 12 +- .../src}/main/resources/usernames.yml | 0 settings.gradle | 3 + .../gg/obsidian/discordbridge/Config.kt | 87 ---- .../discordbridge/DataConfigAccessor.kt | 100 ---- .../gg/obsidian/discordbridge/Plugin.kt | 341 ------------- .../obsidian/discordbridge/UserAliasConfig.kt | 46 -- .../minecraft/CommandListener.kt | 39 -- .../discordbridge/minecraft/EventListener.kt | 178 ------- .../gg/obsidian/discordbridge/utils/Rating.kt | 24 - .../obsidian/discordbridge/utils/UserAlias.kt | 24 - 63 files changed, 1977 insertions(+), 1278 deletions(-) create mode 100644 discordbridge-bukkit/build.gradle create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridgePlugin.kt rename {src/main/kotlin/gg/obsidian/discordbridge/commands => discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge}/DiscordCommandSender.kt (98%) create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Command.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ConsoleSender.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Player.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Scheduler.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Server.kt create mode 100644 discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/World.kt rename {src => discordbridge-bukkit/src}/main/resources/plugin.yml (94%) create mode 100644 discordbridge-core/build.gradle create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/ConfigurationNode.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridge.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/UserAliasConfig.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt rename src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordMessageWrapper.kt (83%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt (73%) rename src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftChatEventWrapper.kt (78%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt (80%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt (100%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt (72%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/controllers/FunCommandsController.kt (77%) rename {src/main/kotlin/gg/obsidian/discordbridge/commands => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers}/IBotController.kt (82%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/commands/controllers/UtilCommandsController.kt (73%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/discord/Connection.kt (79%) rename {src => discordbridge-core/src}/main/kotlin/gg/obsidian/discordbridge/discord/Listener.kt (60%) create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util/ChatColor.kt rename {src/main/kotlin/gg/obsidian/discordbridge/utils => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util}/MarkdownToMinecraftSeralizer.kt (99%) create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util/Rating.kt rename {src/main/kotlin/gg/obsidian/discordbridge/utils => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util}/Respect.kt (52%) rename {src/main/kotlin/gg/obsidian/discordbridge/utils => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util}/Script.kt (57%) create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util/UserAlias.kt rename {src/main/kotlin/gg/obsidian/discordbridge/utils => discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/util}/UtilFunctions.kt (60%) create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ICommand.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ICommandSender.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/IConsoleSender.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/IPlayer.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/IScheduler.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/IServer.kt create mode 100644 discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/wrappers/IWorld.kt rename {src => discordbridge-core/src}/main/resources/8ball.yml (100%) rename {src => discordbridge-core/src}/main/resources/config.yml (100%) rename {src => discordbridge-core/src}/main/resources/f.yml (60%) rename {src => discordbridge-core/src}/main/resources/insult.yml (100%) rename {src => discordbridge-core/src}/main/resources/rate.yml (60%) rename {src => discordbridge-core/src}/main/resources/script.yml (84%) rename {src => discordbridge-core/src}/main/resources/usernames.yml (100%) delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/Config.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/DataConfigAccessor.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/Plugin.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/UserAliasConfig.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/minecraft/CommandListener.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/minecraft/EventListener.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/utils/Rating.kt delete mode 100644 src/main/kotlin/gg/obsidian/discordbridge/utils/UserAlias.kt diff --git a/build.gradle b/build.gradle index ce9bd56..4122ea6 100644 --- a/build.gradle +++ b/build.gradle @@ -11,84 +11,65 @@ 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' +allprojects { + group = 'gg.obsidian' + version = '3.1.0' + description = """Bridge chat between Minecraft and Discord""" + ext.url = 'https://github.com/the-obsidian/DiscordBridge' } -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' +apply plugin: 'java' 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') + compile project('discordbridge-bukkit') + compile project('discordbridge-core') } -compileKotlin { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - - kotlinOptions { - jvmTarget = "1.8" - } -} - -processResources { - filter ReplaceTokens, tokens: [ - 'DESCRIPTION': project.property('description'), - 'URL' : project.property('url'), - 'VERSION' : project.property('version') - ] +sourceSets { + main.java.srcDirs += 'src/main/kotlin' } -//noinspection GroovyAssignabilityCheck -build.finalizedBy(shadowJar) - -shadowJar { - relocate 'org.apache', 'shadow.apache' - relocate 'org.json', 'shadow.json' - classifier 'dist' - dependencies { - //noinspection GroovyAssignabilityCheck - exclude(dependency('org.spigotmc:.*:.*')) +subprojects { + apply plugin: 'java' + apply plugin: 'kotlin' + apply plugin: 'com.github.johnrengelman.shadow' + + processResources { + filter ReplaceTokens, tokens: [ + 'DESCRIPTION': project.property('description'), + 'URL' : project.property('url'), + 'VERSION' : project.property('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 { + dependencies { + include(dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")) + include(dependency('net.dv8tion:JDA:3.3.1_286')) + include(dependency(group: 'org.yaml', name: 'snakeyaml', version: '1.19')) + include(dependency(group: 'org.pegdown', name:'pegdown', version: '1.6.0')) + include(dependency(group: 'commons-lang', name:'commons-lang', version: '2.3')) + include(dependency(group: 'com.michaelwflaherty', name: 'cleverbotapi', version: '1.0.1')) + include(dependency('org.parboiled:parboiled-core')) + include(dependency('org.parboiled:parboiled-java')) + 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')) + include(dependency('com.squareup.okhttp3:okhttp:')) + include(dependency('com.squareup.okio:okio')) + include(dependency('com.neovisionaries:nv-websocket-client')) + include(dependency('org.slf4j:slf4j-api')) + include(dependency('org.json:json')) + } + } -shadowJar.finalizedBy(copyFinalJar) -sourceSets { - main.java.srcDirs += 'src/main/kotlin' -} +} \ No newline at end of file diff --git a/discordbridge-bukkit/build.gradle b/discordbridge-bukkit/build.gradle new file mode 100644 index 0000000..259f237 --- /dev/null +++ b/discordbridge-bukkit/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'kotlin' + +repositories { + mavenCentral() + maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url 'https://github.com/DV8FromTheWorld/Maven-Repository/raw/master/repo' } + jcenter() +} + +dependencies { + compile project(':discordbridge-core') + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile group: 'org.spigotmc', name: 'spigot-api', version: '1.12-R0.1-SNAPSHOT' + + compile files('../libraries/dynmap-api-2.5-SNAPSHOT.jar') +} + +sourceSets { + main.java.srcDirs += 'src/main/kotlin' +} + +shadowJar { + dependencies { + include(project(':discordbridge-core')) + include(dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")) + } +} +build.dependsOn(shadowJar) + +artifacts { + archives shadowJar +} + +// +//compileKotlin { +// kotlinOptions.jvmTarget = "1.8" +//} +//compileTestKotlin { +// kotlinOptions.jvmTarget = "1.8" +//} +// +//shadowJar { +// dependencies { +// include(project(':discordbridge-core')) +// } +//} +//build.dependsOn(shadowJar) \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridgePlugin.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridgePlugin.kt new file mode 100644 index 0000000..e0fc1a1 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridgePlugin.kt @@ -0,0 +1,35 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.wrappers.Server +import org.bukkit.plugin.java.JavaPlugin + +/** + * The primary Plugin class that maintains the plugin's connection with Bukkit + */ +class DiscordBridgePlugin : JavaPlugin() { + + private lateinit var core: DiscordBridge + private lateinit var instance: DiscordBridgePlugin + + fun getPlugin() : DiscordBridgePlugin = instance + + fun getCore() : DiscordBridge = core + + override fun onEnable() { + instance = this + core = DiscordBridge(Server(this, this.server), dataFolder) + core.postInit() + + server.pluginManager.registerEvents(EventListener(core), this) + + getCommand("discord").executor = EventListener(core) + getCommand("f").executor = EventListener(core) + getCommand("rate").executor = EventListener(core) + getCommand("8ball").executor = EventListener(core) + getCommand("insult").executor = EventListener(core) + getCommand("choose").executor = EventListener(core) + getCommand("talk").executor = EventListener(core) + getCommand("roll").executor = EventListener(core) + } + +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordCommandSender.kt similarity index 98% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt rename to discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordCommandSender.kt index 8658bcf..2a2bea7 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/DiscordCommandSender.kt @@ -1,4 +1,4 @@ -package gg.obsidian.discordbridge.commands +package gg.obsidian.discordbridge import net.dv8tion.jda.core.entities.MessageChannel import org.bukkit.Bukkit 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..da57a4c --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt @@ -0,0 +1,54 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.wrappers.ConsoleSender +import gg.obsidian.discordbridge.wrappers.Player +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(val db: DiscordBridge) : Listener, CommandExecutor { + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + return if (sender is org.bukkit.entity.Player) { + val wp = Player(sender) + db.handleCommand(wp, gg.obsidian.discordbridge.wrappers.Command(command), args) + } else { + db.handleCommand(ConsoleSender(db, Bukkit.getConsoleSender()), gg.obsidian.discordbridge.wrappers.Command(command), args) + } + } + + @EventHandler + fun onChat(event: AsyncPlayerChatEvent) { + event.message = db.handlePlayerChat(Player(event.player), event.message, event.isCancelled) + } + + @EventHandler + fun onPlayerJoin(event: PlayerJoinEvent) { + db.handlePlayerJoin(Player(event.player)) + } + + @EventHandler + fun onPlayerLeave(event: PlayerQuitEvent) { + db.handlePlayerQuit(Player(event.player)) + } + + @EventHandler + fun onPlayerDeath(event: PlayerDeathEvent) { + db.handlePlayerDeath(Player(event.entity.player), event.deathMessage) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onDynmapCatEvent(event: DynmapWebChatEvent) { + db.handleDynmapChat(event.name, event.message) + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Command.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Command.kt new file mode 100644 index 0000000..11fe0f2 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Command.kt @@ -0,0 +1,10 @@ +package gg.obsidian.discordbridge.wrappers + +import org.bukkit.command.Command + +class Command(val cmd: Command) : ICommand { + override fun getName(): String { + return cmd.name + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ConsoleSender.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ConsoleSender.kt new file mode 100644 index 0000000..a356c20 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/ConsoleSender.kt @@ -0,0 +1,26 @@ +package gg.obsidian.discordbridge.wrappers + +import gg.obsidian.discordbridge.DiscordBridge +import net.dv8tion.jda.core.entities.MessageChannel +import org.bukkit.command.ConsoleCommandSender +import java.util.* + +class ConsoleSender(val db: DiscordBridge, val bukkitConsoleSender: ConsoleCommandSender) : IConsoleSender { + + 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" + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Player.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Player.kt new file mode 100644 index 0000000..c8ec073 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Player.kt @@ -0,0 +1,31 @@ +package gg.obsidian.discordbridge.wrappers + +import org.bukkit.entity.Player +import java.util.* + +class Player(val bukkitPlayer: Player) : IPlayer { + override fun hasPermission(permission: String): Boolean { + return bukkitPlayer.hasPermission(permission) + } + + override fun getWorld(): IWorld { + return World(bukkitPlayer.world) + } + + override fun isVanished(): Boolean { + return bukkitPlayer.hasMetadata("vanished") && bukkitPlayer.getMetadata("vanished")[0].asBoolean() + } + + override fun getName(): String { + return bukkitPlayer.name + } + + override fun sendMessage(message: String) { + bukkitPlayer.sendMessage(message) + } + + override fun getUUID(): UUID { + return bukkitPlayer.uniqueId + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Scheduler.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Scheduler.kt new file mode 100644 index 0000000..42ccad4 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Scheduler.kt @@ -0,0 +1,12 @@ +package gg.obsidian.discordbridge.wrappers + +import gg.obsidian.discordbridge.DiscordBridgePlugin +import org.bukkit.scheduler.BukkitScheduler + +class Scheduler(val plugin: DiscordBridgePlugin, val bukkitScheduler: BukkitScheduler) : IScheduler { + + override fun runAsyncTask(task: Runnable) { + bukkitScheduler.runTaskAsynchronously(plugin, task) + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Server.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Server.kt new file mode 100644 index 0000000..40184f4 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/Server.kt @@ -0,0 +1,52 @@ +package gg.obsidian.discordbridge.wrappers + +import gg.obsidian.discordbridge.DiscordBridgePlugin +import gg.obsidian.discordbridge.DiscordCommandSender +import net.dv8tion.jda.core.entities.MessageChannel +import org.bukkit.Server +import java.util.* +import java.util.logging.Logger + +class Server(val plugin: DiscordBridgePlugin, val bukkitServer: Server) : IServer { + override fun getRemoteConsoleSender(): IConsoleSender { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getScheduler(): IScheduler { + return Scheduler(plugin, bukkitServer.scheduler) + } + + override fun getVersion(): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getMinecraftShortVersion(): String { + return bukkitServer.bukkitVersion.split("-")[0] + } + + override fun getPlayer(uuid: UUID): IPlayer { + return Player(bukkitServer.getPlayer(uuid)) + } + + override fun getPlayer(name: String): IPlayer { + return Player(bukkitServer.getPlayer(name)) + } + + override fun getOnlinePlayers(): List { + return bukkitServer.onlinePlayers.map { Player(it) } + } + + override fun broadcastMessage(message: String) { + bukkitServer.broadcastMessage(message) + } + + override fun dispatchCommand(channel: MessageChannel, command: String) { + val sender = DiscordCommandSender(channel) + bukkitServer.dispatchCommand(sender, command) + } + + override fun getLogger(): Logger { + return plugin.logger + } + +} \ No newline at end of file diff --git a/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/World.kt b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/World.kt new file mode 100644 index 0000000..a0d5935 --- /dev/null +++ b/discordbridge-bukkit/src/main/kotlin/gg/obsidian/discordbridge/wrappers/World.kt @@ -0,0 +1,10 @@ +package gg.obsidian.discordbridge.wrappers + +import org.bukkit.World + +class World(val bukkitWorld: World) : IWorld { + override fun getName(): String { + return "world" + } + +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/discordbridge-bukkit/src/main/resources/plugin.yml similarity index 94% rename from src/main/resources/plugin.yml rename to discordbridge-bukkit/src/main/resources/plugin.yml index 2e7bdbf..558a316 100644 --- a/src/main/resources/plugin.yml +++ b/discordbridge-bukkit/src/main/resources/plugin.yml @@ -8,7 +8,7 @@ website: '@URL@' loadbefore: [SpaceBukkit, RemoteToolkitPlugin] softdepend: [Multiverse-Core] -main: gg.obsidian.discordbridge.Plugin +main: gg.obsidian.discordbridge.DiscordBridgePlugin 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 diff --git a/discordbridge-core/build.gradle b/discordbridge-core/build.gradle new file mode 100644 index 0000000..af5efd9 --- /dev/null +++ b/discordbridge-core/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'kotlin' + +repositories { + mavenCentral() + maven { url 'https://github.com/DV8FromTheWorld/Maven-Repository/raw/master/repo' } + jcenter() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + 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' + compile group: 'commons-lang', name:'commons-lang', version: '2.3' + compile group: 'com.michaelwflaherty', name: 'cleverbotapi', version: '1.0.1' +} + +sourceSets { + main.java.srcDirs += 'src/main/kotlin' +} + +build.dependsOn(shadowJar) +// +//compileKotlin { +// kotlinOptions.jvmTarget = "1.8" +//} +//compileTestKotlin { +// kotlinOptions.jvmTarget = "1.8" +//} \ No newline at end of file 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..29e7691 --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/ConfigurationNode.kt @@ -0,0 +1,413 @@ +// 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.Rating +import gg.obsidian.discordbridge.util.Respect +import gg.obsidian.discordbridge.util.Script +import gg.obsidian.discordbridge.util.UserAlias +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStreamWriter +import java.util.ArrayList +import java.util.LinkedHashMap + +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor +import org.yaml.snakeyaml.constructor.SafeConstructor +import org.yaml.snakeyaml.error.YAMLException +import org.yaml.snakeyaml.introspector.Property +import org.yaml.snakeyaml.nodes.CollectionNode +import org.yaml.snakeyaml.nodes.MappingNode +import org.yaml.snakeyaml.nodes.Node +import org.yaml.snakeyaml.nodes.NodeTuple +import org.yaml.snakeyaml.nodes.SequenceNode +import org.yaml.snakeyaml.nodes.Tag +import org.yaml.snakeyaml.reader.UnicodeReader +import org.yaml.snakeyaml.representer.Represent +import org.yaml.snakeyaml.representer.Representer + +class ConfigurationNode() : MutableMap { + 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(Rating::class.java, Tag("!!Rating")) +// representer.addClassTag(Respect::class.java, Tag("!!Respect")) +// representer.addClassTag(Script::class.java, Tag("!!Script")) +// representer.addClassTag(UserAlias::class.java, Tag("!!UserAlias")) + + //yaml = Yaml(SafeConstructor(), representer, options) + yaml = Yaml(CustomClassLoaderConstructor(DiscordBridge::class.java.classLoader), representer, options) + } + } + + constructor(f: File): this() { + this.f = f + } + + constructor(map: MutableMap): this() { + if (map == null) { + throw IllegalArgumentException() + } + entries2 = map + } + + constructor(input: InputStream): this() { + load(input) + } + + @SuppressWarnings("unchecked") + fun load(input: InputStream): Boolean { + initparse() + + val o: Any = yaml!!.load(UnicodeReader(input)) + if(o is MutableMap<*, *>) + entries2 = o as MutableMap + return (entries2 != null) + } + + @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) + 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) + val ret = ConfigurationNode(submap).getObject(subpath) + return ret + } + + fun getObject(path: String, default: Any): Any { + return getObject(path) ?: return default + } + + @SuppressWarnings("unchecked") + fun getGeneric(path: String, default: T): T { + val o = getObject(path, default as Any) + return try { + o as T + } catch(e: ClassCastException) { + default + } + } + + fun getInteger(path: String, default: Int): Int { + return Integer.parseInt(getObject(path, default).toString()) + } + + fun getLong(path: String, default: Long): Double { + return getObject(path, default).toString().toLong().toDouble() + } + + fun getFloat(path: String, default: Float): Float { + return getObject(path, default).toString().toFloat() + } + + fun getDouble(path: String, default: Double): Double { + return getObject(path, default).toString().toDouble() + } + + fun getBoolean(path: String, default: Boolean): Boolean { + return getObject(path, default).toString().toBoolean() + } + + fun getString(path: String): String? { + val o = getObject(path) + return o.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 { + val o = getObject(path, default) + return o.toString() + } + + @SuppressWarnings("unchecked") + fun getList(path: String): List { + try { + val list = getObject(path) as List + return 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() + } + } + } + + fun getMapList(path: String): List> { + return getList(path) + } + + fun getNode(path: String): ConfigurationNode? { + var v: MutableMap? = null + v = getGeneric(path, v) + if (v == null) + return null + return ConfigurationNode(v) + } + + @SuppressWarnings("unchecked") + fun getNodes(path: String): List { + val o: List = getList(path) + + val nodes = ArrayList() + for(i in o) { + if (i is MutableMap<*, *>) { + var map: MutableMap + try { + map = i as MutableMap + } catch(e: ClassCastException) { + continue + } + nodes.add(ConfigurationNode(map)) + } + } + return nodes + } + + fun extend(other: MutableMap) { + if (other != null) + extendMap(this, other) + } + + 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)) + } + } + } + + fun createInstance(constructorParameters: Array>, constructorArguments: Array): T? { + val typeName = getString("class") + try { + val mapTypeClass = Class.forName(typeName) + + val constructorParameterWithConfiguration = arrayOfNulls>(constructorParameters.size+1) + for(i in constructorParameters.indices) { constructorParameterWithConfiguration[i] = constructorParameters[i]; } + constructorParameterWithConfiguration[constructorParameterWithConfiguration.size-1] = javaClass + + val constructorArgumentsWithConfiguration = arrayOfNulls(constructorArguments.size+1) + for(i in constructorArguments.indices) { constructorArgumentsWithConfiguration[i] = constructorArguments[i]; } + constructorArgumentsWithConfiguration[constructorArgumentsWithConfiguration.size-1] = this + val constructor = mapTypeClass.getConstructor(*constructorParameterWithConfiguration) + @SuppressWarnings("unchecked") + val t = constructor.newInstance(constructorArgumentsWithConfiguration) as T + return t + } catch (e: Exception) { + // TODO: Remove reference to MapManager. + //Log.severe("Error loading maptype", e) + e.printStackTrace() + } + return null + } + + fun createInstances(path: String, constructorParameters: Array>, constructorArguments: Array): List { + val nodes = getNodes(path) + val instances = ArrayList() + for(node in nodes) { + val instance = node.createInstance(constructorParameters, constructorArguments) + if (instance != null) instances.add(instance) + } + return instances + } + + override val size: Int get() { + return entries2!!.size + } + + override fun isEmpty(): Boolean { + return entries2!!.isEmpty() + } + + override fun containsKey(key: String): Boolean { + return entries2!!.containsKey(key) + } + + override fun containsValue(value: Any): Boolean { + return entries2!!.containsValue(value) + } + + override fun get(key: String): Any? { + return entries2!![key] + } + + override fun put(key: String, value: Any): Any? { + return entries2!!.put(key, value) + } + + override fun remove(key: String): Any? { + return entries2!!.remove(key) + } + + override fun putAll(from: Map) { + entries2!!.putAll(from) + } + + override fun clear() { + entries2!!.clear() + } + + override val keys: MutableSet get() { + return entries2!!.keys + } + + override val values: MutableCollection get() { + return entries2!!.values + } + + override val entries: MutableSet> get() { + return 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 + } + +} \ No newline at end of file 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..e2d83cf --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/DiscordBridge.kt @@ -0,0 +1,465 @@ +package gg.obsidian.discordbridge + +import gg.obsidian.discordbridge.commands.MinecraftChatEventWrapper +import gg.obsidian.discordbridge.commands.MinecraftCommandWrapper +import gg.obsidian.discordbridge.commands.controllers.BotControllerManager +import gg.obsidian.discordbridge.commands.controllers.FunCommandsController +import gg.obsidian.discordbridge.commands.controllers.UtilCommandsController +import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.util.MarkdownToMinecraftSeralizer +import gg.obsidian.discordbridge.util.Respect +import gg.obsidian.discordbridge.util.UserAlias +import gg.obsidian.discordbridge.wrappers.IServer +import gg.obsidian.discordbridge.util.ChatColor as CC +import gg.obsidian.discordbridge.util.UtilFunctions.noSpace +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordChatMessage +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerJoin +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerLeave +import gg.obsidian.discordbridge.util.UtilFunctions.toDiscordPlayerDeath +import gg.obsidian.discordbridge.wrappers.ICommand +import gg.obsidian.discordbridge.wrappers.ICommandSender +import gg.obsidian.discordbridge.wrappers.IPlayer +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.util.logging.Logger +import java.io.IOException +import java.io.FileOutputStream + +class DiscordBridge(private val server: IServer, private val dataFolder: File) { + + private val config: ConfigurationNode + private val users: ConfigurationNode + private val scripts: ConfigurationNode + private val eightball: ConfigurationNode + private val f: ConfigurationNode + private val rate: ConfigurationNode + private val insult: ConfigurationNode + private val pegDownProc = PegDownProcessor() + private val minecraftChatControllerManager = BotControllerManager(this) + private val discordChatControllerManager = BotControllerManager(this) + private val minecraftCommandControllerManager = BotControllerManager(this) + + // Temporary storage for alias linking requests + var requests: MutableList = mutableListOf() + + val logger: Logger = server.getLogger() + + init { + Connection.db = this //TODO: enforce this better + server.getScheduler().runAsyncTask(Connection) + + if (!dataFolder.exists()) dataFolder.mkdirs() + + val cf = File(dataFolder, "config.yml") + if (!createDefaultFileFromResource("/config.yml", cf)) {logger.severe("Could not create default for config.yml")} //return false + val uaf = File(dataFolder, "usernames.yml") + if (!createDefaultFileFromResource("/usernames.yml", uaf)) {logger.severe("Could not create default for usernamess.yml")} // return false + val sf = File(dataFolder, "script.yml") + if (!createDefaultFileFromResource("/script.yml", sf)) {logger.severe("Could not create default for script.yml")} // return false + val ebf = File(dataFolder, "8ball.yml") + if (!createDefaultFileFromResource("/8ball.yml", ebf)) {logger.severe("Could not create default for 8ball.yml")} //return false + val ff = File(dataFolder, "f.yml") + if (!createDefaultFileFromResource("/f.yml", ff)) {logger.severe("Could not create default for f.yml")} // return false + val rf = File(dataFolder, "rate.yml") + if (!createDefaultFileFromResource("/rate.yml", rf)) {logger.severe("Could not create default for rate.yml")} // return false + val inf = File(dataFolder, "insult.yml") + if (!createDefaultFileFromResource("/insult.yml", inf)) {logger.severe("Could not create default for insult.yml")} // return false + + config = ConfigurationNode(cf) + users = ConfigurationNode(uaf) + scripts = ConfigurationNode(sf) + eightball = ConfigurationNode(ebf) + f = ConfigurationNode(ff) + rate = ConfigurationNode(rf) + insult = ConfigurationNode(inf) + + minecraftChatControllerManager.registerController(FunCommandsController(this), chatExclusive = true) + minecraftChatControllerManager.registerController(UtilCommandsController(this), chatExclusive = true) + minecraftCommandControllerManager.registerController(FunCommandsController(this), minecraftExclusive = true) + minecraftCommandControllerManager.registerController(UtilCommandsController(this), minecraftExclusive = true) + discordChatControllerManager.registerController(FunCommandsController(this), discordExclusive = true, chatExclusive = true) + discordChatControllerManager.registerController(UtilCommandsController(this), discordExclusive = true, chatExclusive = true) + } + + fun postInit() { + config.load() + users.load() + scripts.load() + eightball.load() + f.load() + rate.load() + insult.load() + } + + fun logDebug(msg: String) { + if (!getConfig().getBoolean("debug", false)) return + logger.info("[DiscordBridge] $msg") + } + + fun getServer(): IServer = server + fun getPegDownProcessor(): PegDownProcessor = pegDownProc + + fun getConfig(): ConfigurationNode = config + fun getUsersConfig(): ConfigurationNode = users + fun getScriptsConfig(): ConfigurationNode = scripts + fun getEightBallConfig(): ConfigurationNode = eightball + fun getFConfig(): ConfigurationNode = f + fun getRateConfig(): ConfigurationNode = rate + fun getInsultConfig(): ConfigurationNode = insult + + // 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 + //TODO: This is initialized before the config that controls the debug logger is! + logger.info(deffile.path + " not found - creating default") + //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 reload(callback: Runnable) { + config.load() + users.load() + eightball.load() + insult.load() + f.load() + rate.load() + scripts.load() + //if (isMultiverseInstalled) worlds!!.reloadConfig() + UserAliasConfig.load(this) + 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: IPlayer, 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(), found.user.id) + requests.add(ua) + val msg = "Minecraft user '${server.getPlayer(ua.mcUuid).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) discordaliases.add(Pair(server.getPlayer(mcUuid).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 nameMC = server.getPlayer(mcUuid).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 nameMC = server.getPlayer(mcUuid).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 handlePlayerChat(player: IPlayer, 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().getBoolean("messages.chat", true)) return message + if (isCancelled && !getConfig().getBoolean("relay-cancelled-messages", true)) return message + if (player.isVanished() && !getConfig().getBoolean("if-vanished.chat", false)) return message + + // Emoticons! + var 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)) }) + + newMessage = MarkdownToMinecraftSeralizer().toMinecraft(pegDownProc.parseMarkdown(newMessage.toCharArray())) + return newMessage + } + + fun handlePlayerJoin(player: IPlayer) { + val username = player.getName() + val worldname = player.getWorld().getName() + logDebug("Received a join event for $username") + if (!getConfig().getBoolean("messages.join", true)) return + if (player.isVanished() && !getConfig().getBoolean("if-vanished.join", false)) return + + // Get world alias if Multiverse is installed +// if (plugin.isMultiverseInstalled) { +// val worldProperties = plugin.worlds!!.data.get("worlds.$worldname") +// val cls = Class.forName("com.onarandombox.MultiverseCore.WorldProperties") +// val meth: Method = cls.getMethod("getAlias") +// val alias = meth.invoke(worldProperties) +// if (alias is String) worldname = alias +// } + + var formattedMessage = player.toDiscordPlayerJoin(this, worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handlePlayerQuit(player: IPlayer) { + val username = player.getName() + val worldname = player.getWorld().getName() + logDebug("Received a leave event for $username") + if (!getConfig().getBoolean("messages.leave", true)) return + if (player.isVanished() && !getConfig().getBoolean("if-vanished.leave", false)) return + + // Get world alias if Multiverse is installed +// if (plugin.isMultiverseInstalled) { +// val worldProperties = plugin.worlds!!.data.get("worlds.$worldname") +// val cls = Class.forName("com.onarandombox.MultiverseCore.WorldProperties") +// val meth: Method = cls.getMethod("getAlias") +// val alias = meth.invoke(worldProperties) +// if (alias is String) worldname = alias +// } + + var formattedMessage = player.toDiscordPlayerLeave(this, worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handlePlayerDeath(player: IPlayer, deathMessage: String) { + val username = player.getName() + val worldname = player.getWorld().getName() + + logDebug("Received a death event for $username") + if (!getConfig().getBoolean("messages.death", false)) return + if (player.isVanished() && !getConfig().getBoolean("if-vanished.death", false)) return + + // Get world alias if Multiverse is installed +// if (plugin.isMultiverseInstalled) { +// val worldProperties = plugin.worlds!!.data.get("worlds.$worldname") +// val cls = Class.forName("com.onarandombox.MultiverseCore.WorldProperties") +// val meth: Method = cls.getMethod("getAlias") +// val alias = meth.invoke(worldProperties) +// if (alias is String) worldname = alias +// } + + var formattedMessage = deathMessage.toDiscordPlayerDeath(this, username, worldname) + formattedMessage = translateAliasesToDiscord(formattedMessage) + sendToDiscord(formattedMessage, Connection.getRelayChannel()) + } + + fun handleDynmapChat(name: String, message: String) { + sendToDiscord(translateAliasesToDiscord(message.toDiscordChatMessage(this, name, "Dynmap")), Connection.getRelayChannel()) + } + + fun handleCommand(sender: ICommandSender, command: ICommand, args: Array): Boolean { + return minecraftCommandControllerManager.dispatchMessage(MinecraftCommandWrapper(sender, command, args)) + } + +} \ No newline at end of file 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..310aeac --- /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.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(db: DiscordBridge) { + val list = db.getUsersConfig().getList>("aliases") + aliases = list.castTo({UserAlias(it)}) ?: + throw IllegalStateException("usernames.yml could not be read - list items are not properly formatted") + } + + /** + * Adds a new alias to the list and saves the updated list to file + */ + fun add(db: DiscordBridge, ua: UserAlias) { + aliases = aliases.plus(ua) + db.getUsersConfig().put("aliases", aliases) + db.getUsersConfig().save() + db.getUsersConfig().load() + } + + /** + * Removes an alias from the list and saves the updated list to file + */ + fun remove(db: DiscordBridge, ua: UserAlias) { + aliases = aliases.minus(ua) + db.getUsersConfig().put("aliases", aliases) + db.getUsersConfig().save() + db.getUsersConfig().load() + } + + private inline fun List>.castTo(factory: (HashMap) -> T): List { + return this.mapTo(mutableListOf()) { factory(it) }.toList() + } +} \ No newline at end of file diff --git a/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt new file mode 100644 index 0000000..0d7074c --- /dev/null +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt @@ -0,0 +1,84 @@ +package gg.obsidian.discordbridge.commands + +import gg.obsidian.discordbridge.wrappers.IConsoleSender +import net.dv8tion.jda.core.entities.MessageChannel + +class DiscordCommandSender(val sender: IConsoleSender, val channel: MessageChannel) { + + //private val sender:ConsoleCommandSender = Bukkit.getServer().consoleSender + + init { + + } + + fun sendMessage(message: String?) { + channel.sendMessage(message).queue() + } + + fun sendMessage(messages: Array?) { + if (messages != null) + for (m in messages) channel.sendMessage(m) + } + +// override fun addAttachment(plugin: Plugin?): PermissionAttachment { +// return sender.addAttachment(plugin) +// } +// +// override fun addAttachment(plugin: Plugin?, ticks: Int): PermissionAttachment { +// return sender.addAttachment(plugin, ticks) +// } +// +// override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean): PermissionAttachment { +// return sender.addAttachment(plugin, name, value) +// } +// +// override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean, ticks: Int): PermissionAttachment { +// return sender.addAttachment(plugin, name, value, ticks) +// } +// +// override fun getEffectivePermissions(): MutableSet { +// return sender.effectivePermissions +// } + +// fun getName(): String { +// return sender.name +// } +// +// override fun getServer(): Server { +// return sender.server +// } +// +// override fun hasPermission(name: String?): Boolean { +// return sender.hasPermission(name) +// } +// +// override fun hasPermission(perm: Permission?): Boolean { +// return sender.hasPermission(perm) +// } +// +// override fun isOp(): Boolean { +// return sender.isOp +// } +// +// override fun isPermissionSet(name: String?): Boolean { +// return sender.isPermissionSet(name) +// } +// +// override fun isPermissionSet(perm: Permission?): Boolean { +// return sender.isPermissionSet(perm) +// } +// +// override fun recalculatePermissions() { +// return sender.recalculatePermissions() +// } +// +// override fun removeAttachment(attachment: PermissionAttachment?) { +// return sender.removeAttachment(attachment) +// } +// +// override fun setOp(value: Boolean) { +// return sender.setOp(value) +// } + + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordMessageWrapper.kt similarity index 83% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordMessageWrapper.kt index c00ff01..30cd2d8 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/MessageWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordMessageWrapper.kt @@ -1,6 +1,6 @@ package gg.obsidian.discordbridge.commands -import gg.obsidian.discordbridge.Config +import gg.obsidian.discordbridge.DiscordBridge import net.dv8tion.jda.core.entities.ChannelType import net.dv8tion.jda.core.entities.Message import net.dv8tion.jda.core.entities.MessageChannel @@ -10,7 +10,7 @@ import net.dv8tion.jda.core.entities.MessageChannel * * @param originalMessage the underlying Message instance */ -class MessageWrapper(val originalMessage: Message) : IEventWrapper { +class DiscordMessageWrapper(val db: DiscordBridge, 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 == db.getConfig().getString("server-id") && originalMessage.isFromType(ChannelType.TEXT) - && originalMessage.textChannel.name.equals(Config.CHANNEL, true) + && originalMessage.textChannel.name.equals(db.getConfig().getString("channel"), true) /** * The message of this event diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt similarity index 73% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt index 5116f7c..bc1c27c 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/IEventWrapper.kt @@ -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/commands/MinecraftChatEventWrapper.kt similarity index 78% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftChatEventWrapper.kt index 9166b38..1b031f7 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/AsyncPlayerChatEventWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftChatEventWrapper.kt @@ -1,37 +1,37 @@ package gg.obsidian.discordbridge.commands import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.wrappers.IPlayer import net.dv8tion.jda.core.entities.MessageChannel -import org.bukkit.event.player.AsyncPlayerChatEvent /** * A wrapper for Bukkit's AsyncPlayerChatEvent class * * @param event the underlying AsyncPlayerChatEvent instance */ -class AsyncPlayerChatEventWrapper(val event: AsyncPlayerChatEvent) : IEventWrapper { +class MinecraftChatEventWrapper(val player: IPlayer, 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,7 +42,7 @@ 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 */ diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt similarity index 80% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt index 141817b..abf0bf9 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/MinecraftCommandWrapper.kt @@ -1,10 +1,10 @@ package gg.obsidian.discordbridge.commands import gg.obsidian.discordbridge.discord.Connection +import gg.obsidian.discordbridge.wrappers.ICommandSender +import gg.obsidian.discordbridge.wrappers.ICommand +import gg.obsidian.discordbridge.wrappers.IPlayer 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 @@ -13,7 +13,7 @@ import org.bukkit.entity.Player * @param command 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: ICommandSender, val command: ICommand, val args: Array) : IEventWrapper { /** * The Minecraft username of the command sender @@ -21,7 +21,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() = if (sender is IPlayer) sender.getName() else "Console" /** * Returns a space-delimited string of all the arguments passed with the command * @@ -40,7 +40,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,7 +51,7 @@ 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? IPlayer)?.getUUID()?.toString() ?: "" /** * Always returns true for this wrapper type */ diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/ChatExclusiveCommand.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/DiscordExclusiveCommand.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/MinecraftExclusiveCommand.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/PrivateResponse.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt similarity index 100% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/TaggedResponse.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt similarity index 72% rename from src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt rename to discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt index 8f09c83..2de6258 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt +++ b/discordbridge-core/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt @@ -1,23 +1,21 @@ package gg.obsidian.discordbridge.commands.controllers -import gg.obsidian.discordbridge.Config -import gg.obsidian.discordbridge.Plugin +import gg.obsidian.discordbridge.DiscordBridge import gg.obsidian.discordbridge.commands.* import gg.obsidian.discordbridge.commands.annotations.* 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.ChatColor as CC +import gg.obsidian.discordbridge.util.MarkdownToMinecraftSeralizer +import gg.obsidian.discordbridge.util.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 kotlin.collections.HashMap /** * A class that manages an assortment of IBotControllers and allows dynamic access to a configurable assortment @@ -26,7 +24,7 @@ import org.bukkit.ChatColor as CC * @param plugin a reference to the base Plugin object * @see IBotController */ -class BotControllerManager(val plugin: Plugin) { +class BotControllerManager(val db: DiscordBridge) { private val commands: MutableMap = mutableMapOf() private val controllers: MutableMap, IBotController> = mutableMapOf() @@ -97,9 +95,9 @@ class BotControllerManager(val plugin: Plugin) { // Short circuit if event was a Minecraft command if (event is MinecraftCommandWrapper) { - val command = commands[event.command.name] + val command = commands[event.command.getName()] if (command == null) { - commandNotFound(event, event.command.name) + commandNotFound(event, event.command.getName()) return true } return invokeBotCommand(command, controllers, event, event.args.asList().toTypedArray()) @@ -109,19 +107,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 (db.getConfig().getString("command-prefix", "").isNotBlank() && event.rawMessage.startsWith(db.getConfig().getString("command-prefix", ""))) { + val split = event.rawMessage.replaceFirst(db.getConfig().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("@${db.getConfig().getString("username", "DiscordBridge").noSpace()} ")) { + val split = event.rawMessage.replaceFirst("@${db.getConfig().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 +136,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