├── .gitmodules ├── 1.12.2 ├── gradle.properties ├── src │ └── main │ │ └── kotlin │ │ └── matterlink │ │ ├── Constants.kt │ │ ├── command │ │ ├── MatterLinkCommand.kt │ │ ├── AuthCommand.kt │ │ └── MatterLinkCommandSender.kt │ │ ├── EventHandler.kt │ │ └── MatterLink.kt └── build.gradle ├── 1.9.4 ├── gradle.properties ├── src │ └── main │ │ └── kotlin │ │ └── matterlink │ │ ├── Constants.kt │ │ ├── command │ │ ├── MatterLinkCommand.kt │ │ ├── AuthCommand.kt │ │ └── MatterLinkCommandSender.kt │ │ ├── EventHandler.kt │ │ └── MatterLink.kt └── build.gradle ├── 1.7.10 ├── gradle.properties ├── src │ ├── templates │ │ └── kotlin │ │ │ └── matterlink │ │ │ └── Constants.kt │ └── main │ │ └── kotlin │ │ └── matterlink │ │ ├── command │ │ ├── AuthCommand.kt │ │ ├── MatterLinkCommand.kt │ │ └── MatterLinkCommandSender.kt │ │ ├── EventHandler.kt │ │ └── MatterLink.kt └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── core ├── src │ └── main │ │ ├── kotlin │ │ └── matterlink │ │ │ ├── Logger.kt │ │ │ ├── api │ │ │ ├── Config.kt │ │ │ ├── ApiMessage.kt │ │ │ └── MessageHandler.kt │ │ │ ├── jenkins │ │ │ ├── BuildWithDetails.kt │ │ │ ├── Artifact.kt │ │ │ ├── Job.kt │ │ │ ├── Build.kt │ │ │ └── JenkinsServer.kt │ │ │ ├── update │ │ │ ├── CurseFile.kt │ │ │ └── UpdateChecker.kt │ │ │ ├── handlers │ │ │ ├── TickHandler.kt │ │ │ ├── ProgressHandler.kt │ │ │ ├── ChatProcessor.kt │ │ │ ├── DeathHandler.kt │ │ │ ├── JoinLeaveHandler.kt │ │ │ ├── LocationHandler.kt │ │ │ └── ServerChatHandler.kt │ │ │ ├── bridge │ │ │ ├── command │ │ │ │ ├── HelpCommand.kt │ │ │ │ ├── IMinecraftCommandSender.kt │ │ │ │ ├── RequestPermissionsCommand.kt │ │ │ │ ├── IBridgeCommand.kt │ │ │ │ ├── AuthBridgeCommand.kt │ │ │ │ ├── CustomCommand.kt │ │ │ │ └── BridgeCommandRegistry.kt │ │ │ └── MessageHandlerInst.kt │ │ │ ├── IMatterLink.kt │ │ │ ├── command │ │ │ ├── CommandCoreML.kt │ │ │ └── CommandCoreAuth.kt │ │ │ ├── config │ │ │ ├── PermissionConfig.kt │ │ │ ├── IdentitiesConfig.kt │ │ │ └── CommandConfig.kt │ │ │ ├── PasteUtil.kt │ │ │ ├── Util.kt │ │ │ └── Area.kt │ │ └── resources │ │ └── mcmod.info └── build.gradle ├── gradle.properties ├── TODO.MD ├── scripts ├── test7.sh ├── start.sh ├── test12.sh └── test9.sh ├── Jenkinsfile ├── LICENSE ├── .gitignore ├── gradlew.bat ├── gradlew ├── README.md └── matterbridge-sample.toml /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Jankson"] 2 | path = Jankson 3 | url = https://github.com/falkreon/Jankson.git -------------------------------------------------------------------------------- /1.12.2/gradle.properties: -------------------------------------------------------------------------------- 1 | mc_version = 1.12.2 2 | mcp_mappings = stable_39 3 | forge_version = 14.23.1.2599 -------------------------------------------------------------------------------- /1.9.4/gradle.properties: -------------------------------------------------------------------------------- 1 | mc_version = 1.9.4 2 | mcp_mappings = stable_26 3 | forge_version = 12.17.0.2051 4 | -------------------------------------------------------------------------------- /1.7.10/gradle.properties: -------------------------------------------------------------------------------- 1 | mc_version = 1.7.10 2 | mcp_mappings = stable_12 3 | forge_version = 10.13.4.1614 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elytra/MatterLink/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'MatterLink' 2 | include 'core' 3 | include 'Jankson' 4 | include '1.12.2', '1.9.4', '1.7.10' -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/Logger.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | interface Logger { 4 | fun info(message: String) 5 | fun debug(message: String) 6 | fun error(message: String) 7 | fun warn(message: String) 8 | fun trace(message: String) 9 | } -------------------------------------------------------------------------------- /1.7.10/src/templates/kotlin/matterlink/Constants.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | const val MODID = "matterlink" 4 | const val NAME = "MatterLink" 5 | const val MODVERSION = "@MODVERSION@" 6 | const val MCVERSION = "@MCVERSION@" 7 | const val BUILD_NUMBER = @BUILD_NUMBER@ 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 19 16:56:53 EST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip 7 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/api/Config.kt: -------------------------------------------------------------------------------- 1 | package matterlink.api 2 | 3 | data class Config( 4 | var url: String = "", 5 | var token: String = "", 6 | var announceConnect: Boolean = true, 7 | var announceDisconnect: Boolean = true, 8 | var reconnectWait: Long = 500, 9 | var systemUser: String = "Server" 10 | ) -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/Constants.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | const val MODID = "matterlink" 4 | const val NAME = "MatterLink" 5 | const val MODVERSION = "@MODVERSION@" 6 | const val MCVERSION = "@MCVERSION@" 7 | const val DEPENDENCIES = "required-after:forgelin@[@FORGELIN-VERSION@,);" 8 | const val BUILD_NUMBER = -1//@BUILD_NUMBER@ 9 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/jenkins/BuildWithDetails.kt: -------------------------------------------------------------------------------- 1 | package matterlink.jenkins 2 | 3 | import kotlinx.serialization.Serializable 4 | import java.util.Date 5 | 6 | @Serializable 7 | data class BuildWithDetails( 8 | val number: Int, 9 | val url: String, 10 | val artifacts: List, 11 | val timestamp: Date 12 | ) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | modName = MatterLink 2 | modVersion = 1.6.4 3 | forgelinVersion = 1.8.2 4 | kotlinVersion = 1.3.10 5 | coroutinesVersion = 1.0.1 6 | serializationVersion = 0.9.1 7 | shadowVersion = 2.0.2 8 | fuelVersion = 8690665998 9 | resultVersion = 2.0.0 10 | cursegradleVersion = 1.1.2 11 | curseId = 287323 12 | curseReleaseType = beta -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/jenkins/Artifact.kt: -------------------------------------------------------------------------------- 1 | package matterlink.jenkins 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Created by nikky on 03/02/18. 7 | * @author Nikky 8 | */ 9 | 10 | @Serializable 11 | data class Artifact( 12 | val displayPath: String, 13 | val fileName: String, 14 | val relativePath: String 15 | ) -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/Constants.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | const val MODID = "matterlink" 4 | const val NAME = "MatterLink" 5 | const val MODVERSION = "@MODVERSION@" 6 | const val MCVERSION = "@MCVERSION@" 7 | const val DEPENDENCIES = "required-after:forgelin@[@FORGELIN-VERSION@,);required-after:forge@[@FORGE-VERSION@,);" 8 | const val BUILD_NUMBER = -1//@BUILD_NUMBER@ 9 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/update/CurseFile.kt: -------------------------------------------------------------------------------- 1 | package matterlink.update 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class CurseFile( 7 | val downloadUrl: String, 8 | val fileName: String, 9 | val fileNameOnDisk: String, 10 | val gameVersion: List, 11 | val releaseType: String, 12 | val fileStatus: String 13 | ) -------------------------------------------------------------------------------- /TODO.MD: -------------------------------------------------------------------------------- 1 | # Replacement for guava 2 | 3 | # using Fuel everywhere to simplify code 4 | 5 | 6 | # Adittional player values 7 | 8 | 9 | add optional feature: 10 | GET json dictionary of player values like TEAM, NICK, TOWN 11 | from url based on uuid / playername 12 | 13 | config input: `http://rest.wurmcraft.com/user/find/{UUID}` 14 | url: `http://rest.wurmcraft.com/user/find/148cf139-dd14-4bf4-97a2-08305dfef0a9` -------------------------------------------------------------------------------- /core/src/main/resources/mcmod.info: -------------------------------------------------------------------------------- 1 | [{ 2 | "modid": "matterlink", 3 | "name": "MatterLink", 4 | "description": "Minecraft Server Matterbridge link, Multi-Platform chat", 5 | "version": "${version}", 6 | "mcversion": "${mcversion}", 7 | "url": "https://github.com/elytra/MatterLink", 8 | "authorList":["NikkyAi", "Arcanitor"], 9 | "credits": "Blame Nikky for talking me into this. \n42wim for creating matterbridge \nUna, Falkreon and capitalthree's patience", 10 | "dependencies": ["forgelin"] 11 | }] -------------------------------------------------------------------------------- /scripts/test7.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" 3 | 4 | RUN="$PWD/run/1.7.10" 5 | 6 | rm -rf "$RUN/mods" 7 | mkdir -p "$RUN/mods" 8 | 9 | "$PWD/gradlew" :1.7.10:clean :1.7.10:build && cp -f $DIR/1.7.10/build/libs/MatterLink-1.7.10-*-dev.jar "$RUN/mods" 10 | if [ ! $? -eq 0 ]; then 11 | echo "Error compiling matterlink" 12 | exit 1 13 | fi 14 | 15 | cd "$RUN" 16 | 17 | curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.7.10-10.13.4.1614-1.7.10/forge-1.7.10-10.13.4.1614-1.7.10-installer.jar" 18 | 19 | $DIR/scripts/start.sh 20 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | java -jar forge-installer.jar --installServer 4 | 5 | FORGE_FILE=`grep "The server installed successfully, you should now be able to run the file " forge-installer.jar.log | tail -1` 6 | FORGE_FILE=${FORGE_FILE#"The server installed successfully, you should now be able to run the file "} 7 | echo $FORGE_FILE 8 | 9 | cp -f "$FORGE_FILE" forge.jar 10 | if [ ! $? -eq 0 ]; then 11 | echo "Error installing forge" 12 | exit 1 13 | fi 14 | 15 | echo "installed forge" 16 | 17 | cp ../eula.txt . 18 | 19 | mkdir -p config 20 | rm -rf config/matterlink 21 | cp -r ../matterlink_config config/matterlink 22 | 23 | java -jar forge.jar -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/jenkins/Job.kt: -------------------------------------------------------------------------------- 1 | package matterlink.jenkins 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Created by nikky on 03/02/18. 7 | * @author Nikky 8 | */ 9 | 10 | @Serializable 11 | data class Job( 12 | val url: String, 13 | val name: String, 14 | val fullName: String, 15 | val displayName: String, 16 | val fullDisplayName: String, 17 | val builds: List?, 18 | val lastSuccessfulBuild: Build?, 19 | val lastStableBuild: Build? 20 | ) { 21 | fun getBuildByNumber(build: Int, userAgent: String): BuildWithDetails? { 22 | return builds?.find { it.number == build }?.details(userAgent) 23 | } 24 | } -------------------------------------------------------------------------------- /scripts/test12.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" 3 | 4 | RUN="$PWD/run/1.12.2" 5 | 6 | rm -rf "$RUN/mods" 7 | mkdir -p "$RUN/mods" 8 | 9 | "$PWD/gradlew" :1.12.2:clean :1.12.2:build && cp -f 1.12.2/build/libs/MatterLink-1.12.2-*-dev.jar "$RUN/mods" 10 | if [ ! $? -eq 0 ]; then 11 | echo "Error compiling matterlink" 12 | exit 1 13 | fi 14 | 15 | cd "$RUN" 16 | 17 | curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.12.2-14.23.5.2768/forge-1.12.2-14.23.5.2768-installer.jar" 18 | curl -L -o "$RUN/mods/Forgelin.jar" "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2640952/download" 19 | 20 | $DIR/scripts/start.sh 21 | 22 | 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/TickHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.update.UpdateChecker 4 | 5 | /** 6 | * Created by nikky on 21/02/18. 7 | * @author Nikky 8 | * @version 1.0 9 | */ 10 | object TickHandler { 11 | var tickCounter = 0 12 | private set 13 | private var accumulator = 0 14 | private const val updateInterval = 12 * 60 * 60 * 20 15 | suspend fun handleTick() { 16 | tickCounter++ 17 | // if (tickCounter % 100 == 0) { 18 | // MessageHandlerInst.checkConnection() 19 | // } 20 | 21 | ServerChatHandler.writeIncomingToChat() 22 | 23 | if (accumulator++ > updateInterval) { 24 | accumulator -= updateInterval 25 | UpdateChecker.check() 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/ProgressHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.antiping 4 | import matterlink.config.cfg 5 | import matterlink.stripColorOut 6 | 7 | object ProgressHandler { 8 | 9 | suspend fun handleProgress( 10 | name: String, message: String, display: String, 11 | x: Int, y: Int, z: Int, 12 | dimension: Int 13 | ) { 14 | if (!cfg.outgoing.advancements) return 15 | val usr = name.stripColorOut.antiping 16 | LocationHandler.sendToLocations( 17 | msg = "$usr $message $display".stripColorOut, 18 | x = x, y = y, z = z, dimension = dimension, 19 | event = ChatEvent.ADVANCEMENT, 20 | cause = "Progress Event by $usr", 21 | systemuser = true 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /scripts/test9.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" 3 | 4 | RUN="$PWD/run/1.9.4" 5 | 6 | rm -rf "$RUN/mods" 7 | mkdir -p "$RUN/mods" 8 | 9 | "$PWD/gradlew" :1.9.4:clean :1.9.4:build && cp -f 1.9.4/build/libs/MatterLink-1.9.4-*-dev.jar "$RUN/mods" 10 | if [ ! $? -eq 0 ]; then 11 | echo "Error compiling matterlink" 12 | exit 1 13 | fi 14 | 15 | cd "$RUN" 16 | 17 | curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.9.4-12.17.0.2051/forge-1.9.4-12.17.0.2051-installer.jar" 18 | curl -L -o "$RUN/mods/Forgelin.jar" "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2640952/download" # "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2573311/download" 19 | 20 | $DIR/scripts/start.sh 21 | 22 | 23 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage("init") { 5 | steps { 6 | sh 'git submodule update --init --recursive' 7 | } 8 | } 9 | stage("1.7.10") { 10 | steps { 11 | sh './gradlew :1.7.10:setupCiWorkspace' 12 | sh './gradlew :1.7.10:clean' 13 | sh './gradlew :1.7.10:build' 14 | archiveArtifacts artifacts: '1.7.10/build/libs/*jar' 15 | } 16 | } 17 | stage("1.9.4") { 18 | steps { 19 | sh './gradlew :1.9.4:setupCiWorkspace' 20 | sh './gradlew :1.9.4:clean' 21 | sh './gradlew :1.9.4:build' 22 | archiveArtifacts artifacts: '1.9.4/build/libs/*jar' 23 | } 24 | } 25 | stage("1.12.2") { 26 | steps { 27 | sh './gradlew :1.12.2:setupCiWorkspace' 28 | sh './gradlew :1.12.2:clean' 29 | sh './gradlew :1.12.2:build' 30 | archiveArtifacts artifacts: '1.12.2/build/libs/*jar' 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/HelpCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.config.cfg 4 | 5 | object HelpCommand : IBridgeCommand() { 6 | override val help: String = "Returns the help string for the given command. Syntax: help " 7 | override val permLevel: Double 8 | get() = cfg.command.defaultPermUnauthenticated 9 | 10 | override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean { 11 | val msg: String = when { 12 | args.isEmpty() -> 13 | "Available commands: ${BridgeCommandRegistry.getCommandList(IBridgeCommand.getPermLevel(env.uuid))}" 14 | else -> args.split(" ", ignoreCase = false) 15 | .joinToString(separator = "\n") { 16 | "$it: ${BridgeCommandRegistry.getHelpString(it)}" 17 | } 18 | } 19 | env.respond( 20 | text = msg, 21 | cause = "Help Requested $args" 22 | ) 23 | return true 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 NikkyAi & Arcan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/jenkins/Build.kt: -------------------------------------------------------------------------------- 1 | package matterlink.jenkins 2 | 3 | import com.github.kittinunf.fuel.httpGet 4 | import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf 5 | import com.github.kittinunf.result.Result 6 | import kotlinx.serialization.json.JSON 7 | import matterlink.logger 8 | 9 | 10 | /** 11 | * Created by nikky on 03/02/18. 12 | * @author Nikky 13 | */ 14 | 15 | //@JsonIgnoreProperties(ignoreUnknown = true) 16 | data class Build( 17 | val number: Int, 18 | val url: String 19 | ) { 20 | fun details(userAgent: String): BuildWithDetails? { 21 | val (request, response, result) = "$url/api/json" 22 | .httpGet() 23 | .header("User-Agent" to userAgent) 24 | .responseObject(kotlinxDeserializerOf(loader = BuildWithDetails.serializer(), json = JSON.nonstrict)) 25 | return when (result) { 26 | is Result.Success -> { 27 | result.value 28 | } 29 | is Result.Failure -> { 30 | logger.error(result.error.toString()) 31 | null 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/jenkins/JenkinsServer.kt: -------------------------------------------------------------------------------- 1 | package matterlink.jenkins 2 | 3 | 4 | import com.github.kittinunf.fuel.httpGet 5 | import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf 6 | import com.github.kittinunf.result.Result 7 | import kotlinx.serialization.json.JSON 8 | import matterlink.logger 9 | 10 | /** 11 | * Created by nikky on 03/02/18. 12 | * @author Nikky 13 | */ 14 | 15 | 16 | class JenkinsServer(val url: String) { 17 | 18 | fun getUrl(job: String) = url + "/job/" + job.replace("/", "/job/") 19 | 20 | fun getJob(job: String, userAgent: String): Job? { 21 | val requestURL = getUrl(job) + "/api/json" 22 | val (_, _, result) = requestURL 23 | .httpGet() 24 | .header("User-Agent" to userAgent) 25 | .responseObject(kotlinxDeserializerOf(loader = Job.serializer(), json = JSON.nonstrict)) 26 | return when (result) { 27 | is Result.Success -> { 28 | result.value 29 | } 30 | is Result.Failure -> { 31 | logger.error(result.error.toString()) 32 | null 33 | } 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /1.7.10/src/main/kotlin/matterlink/command/AuthCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import net.minecraft.command.CommandBase 4 | import net.minecraft.command.ICommandSender 5 | import net.minecraft.command.WrongUsageException 6 | import net.minecraft.entity.player.EntityPlayer 7 | import net.minecraft.util.ChatComponentText 8 | 9 | 10 | object AuthCommand : CommandBase() { 11 | override fun getCommandName(): String { 12 | return CommandCoreAuth.name 13 | } 14 | 15 | override fun getCommandUsage(sender: ICommandSender): String { 16 | return CommandCoreAuth.usage 17 | } 18 | 19 | override fun getCommandAliases(): List { 20 | return CommandCoreAuth.aliases 21 | } 22 | 23 | override fun processCommand(sender: ICommandSender, args: Array) { 24 | if (args.isEmpty()) { 25 | throw WrongUsageException("Invalid command! Valid uses: ${this.getCommandUsage(sender)}") 26 | } 27 | 28 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 29 | val reply = CommandCoreAuth.execute(args, sender.commandSenderName, uuid) 30 | 31 | if (reply.isNotEmpty()) { 32 | sender.addChatMessage(ChatComponentText(reply)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/ChatProcessor.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.bridge.command.BridgeCommandRegistry 4 | import matterlink.logger 5 | import java.util.UUID 6 | 7 | object ChatProcessor { 8 | /** 9 | * @return cancel message flag 10 | */ 11 | suspend fun sendToBridge( 12 | user: String, 13 | msg: String, 14 | x: Int, 15 | y: Int, 16 | z: Int, 17 | dimension: Int?, 18 | event: ChatEvent, 19 | uuid: UUID? = null 20 | ): Boolean { 21 | //TODO: pass message to Locations 22 | logger.info("position: $x $y $z dimension: $dimension") 23 | val message = msg.trim() 24 | if (uuid != null && BridgeCommandRegistry.handleCommand(message, user, uuid)) return true 25 | when { 26 | message.isNotBlank() -> LocationHandler.sendToLocations( 27 | user = user, 28 | msg = message, 29 | x = x, y = y, z = z, dimension = dimension, 30 | event = event, 31 | cause = "Message from $user", 32 | uuid = uuid 33 | ) 34 | 35 | 36 | else -> logger.warn("WARN: dropped blank message by '$user'") 37 | } 38 | return false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/DeathHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.antiping 4 | import matterlink.config.cfg 5 | import matterlink.stripColorOut 6 | import java.util.Random 7 | 8 | object DeathHandler { 9 | private val random = Random() 10 | 11 | suspend fun handleDeath( 12 | player: String, 13 | deathMessage: String, 14 | damageType: String, 15 | x: Int, y: Int, z: Int, 16 | dimension: Int 17 | ) { 18 | if (cfg.outgoing.death.enable) { 19 | var msg = deathMessage.stripColorOut.replace(player, player.stripColorOut.antiping) 20 | if (cfg.outgoing.death.damageType) { 21 | val emojis = cfg.outgoing.death.damageTypeMapping[damageType] 22 | ?: arrayOf("\uD83D\uDC7B unknown type '$damageType'") 23 | val damageEmoji = emojis[random.nextInt(emojis.size)] 24 | msg += " $damageEmoji" 25 | } 26 | LocationHandler.sendToLocations( 27 | msg = msg, 28 | x = x, y = y, z = z, dimension = dimension, 29 | event = ChatEvent.DEATH, 30 | cause = "Death Event of $player", 31 | systemuser = true 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /1.7.10/src/main/kotlin/matterlink/command/MatterLinkCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.minecraft.command.CommandBase 5 | import net.minecraft.command.ICommandSender 6 | import net.minecraft.command.WrongUsageException 7 | import net.minecraft.entity.player.EntityPlayer 8 | import net.minecraft.util.ChatComponentText 9 | 10 | 11 | object MatterLinkCommand : CommandBase() { 12 | override fun getCommandName(): String { 13 | return CommandCoreML.name 14 | } 15 | 16 | override fun getCommandUsage(sender: ICommandSender): String { 17 | return CommandCoreML.usage 18 | } 19 | 20 | override fun getCommandAliases(): List { 21 | return CommandCoreML.aliases 22 | } 23 | 24 | override fun processCommand(sender: ICommandSender, args: Array) = runBlocking { 25 | if (args.isEmpty()) { 26 | throw WrongUsageException("Invalid command! Valid uses: ${getCommandUsage(sender)}") 27 | } 28 | 29 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 30 | val reply = CommandCoreML.execute(args, sender.commandSenderName, uuid) 31 | 32 | if (reply.isNotEmpty()) { 33 | sender.addChatMessage(ChatComponentText(reply)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/command/MatterLinkCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.minecraft.command.CommandBase 5 | import net.minecraft.command.ICommandSender 6 | import net.minecraft.command.WrongUsageException 7 | import net.minecraft.entity.player.EntityPlayer 8 | import net.minecraft.server.MinecraftServer 9 | import net.minecraft.util.text.TextComponentString 10 | 11 | 12 | object MatterLinkCommand : CommandBase() { 13 | override fun getName(): String { 14 | return CommandCoreML.name 15 | } 16 | 17 | override fun getUsage(sender: ICommandSender): String { 18 | return CommandCoreML.usage 19 | } 20 | 21 | override fun getAliases(): List { 22 | return CommandCoreML.aliases 23 | } 24 | 25 | override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array) = runBlocking { 26 | if (args.isEmpty()) { 27 | throw WrongUsageException("Invalid command! Valid uses: ${getUsage(sender)}") 28 | } 29 | 30 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 31 | val reply = CommandCoreML.execute(args, sender.name, uuid) 32 | 33 | if (reply.isNotEmpty() && sender.sendCommandFeedback()) { 34 | sender.sendMessage(TextComponentString(reply)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/command/MatterLinkCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.minecraft.command.CommandBase 5 | import net.minecraft.command.ICommandSender 6 | import net.minecraft.command.WrongUsageException 7 | import net.minecraft.entity.player.EntityPlayer 8 | import net.minecraft.server.MinecraftServer 9 | import net.minecraft.util.text.TextComponentString 10 | 11 | 12 | object MatterLinkCommand : CommandBase() { 13 | override fun getCommandName(): String { 14 | return CommandCoreML.name 15 | } 16 | 17 | override fun getCommandUsage(sender: ICommandSender): String { 18 | return CommandCoreML.usage 19 | } 20 | 21 | override fun getCommandAliases(): List { 22 | return CommandCoreML.aliases 23 | } 24 | 25 | override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array) = runBlocking { 26 | if (args.isEmpty()) { 27 | throw WrongUsageException("Invalid command! Valid uses: ${getCommandUsage(sender)}") 28 | } 29 | 30 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 31 | val reply = CommandCoreML.execute(args, sender.name, uuid) 32 | 33 | if (reply.isNotEmpty() && sender.sendCommandFeedback()) { 34 | sender.addChatMessage(TextComponentString(reply)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/command/AuthCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import net.minecraft.command.CommandBase 4 | import net.minecraft.command.ICommandSender 5 | import net.minecraft.command.WrongUsageException 6 | import net.minecraft.entity.player.EntityPlayer 7 | import net.minecraft.server.MinecraftServer 8 | import net.minecraft.util.text.TextComponentString 9 | 10 | 11 | object AuthCommand : CommandBase() { 12 | override fun getName(): String { 13 | return CommandCoreAuth.name 14 | } 15 | 16 | override fun getUsage(sender: ICommandSender): String { 17 | return CommandCoreAuth.usage 18 | } 19 | 20 | override fun getAliases(): List { 21 | return CommandCoreAuth.aliases 22 | } 23 | 24 | override fun getRequiredPermissionLevel(): Int { 25 | return 0 26 | } 27 | 28 | override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array) { 29 | if (args.isEmpty()) { 30 | throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}") 31 | } 32 | 33 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 34 | val reply = CommandCoreAuth.execute(args, sender.name, uuid) 35 | 36 | if (reply.isNotEmpty() && sender.sendCommandFeedback()) { 37 | sender.sendMessage(TextComponentString(reply)) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/command/AuthCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import net.minecraft.command.CommandBase 4 | import net.minecraft.command.ICommandSender 5 | import net.minecraft.command.WrongUsageException 6 | import net.minecraft.entity.player.EntityPlayer 7 | import net.minecraft.server.MinecraftServer 8 | import net.minecraft.util.text.TextComponentString 9 | 10 | 11 | object AuthCommand : CommandBase() { 12 | override fun getCommandName(): String { 13 | return CommandCoreAuth.name 14 | } 15 | 16 | override fun getCommandUsage(sender: ICommandSender): String { 17 | return CommandCoreAuth.usage 18 | } 19 | 20 | override fun getCommandAliases(): List { 21 | return CommandCoreAuth.aliases 22 | } 23 | 24 | override fun getRequiredPermissionLevel(): Int { 25 | return 0 26 | } 27 | 28 | override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array) { 29 | if (args.isEmpty()) { 30 | throw WrongUsageException("Invalid command! Valid uses: ${this.getCommandUsage(sender)}") 31 | } 32 | 33 | val uuid = (sender as? EntityPlayer)?.uniqueID?.toString() 34 | val reply = CommandCoreAuth.execute(args, sender.name, uuid) 35 | 36 | if (reply.isNotEmpty() && sender.sendCommandFeedback()) { 37 | sender.addChatMessage(TextComponentString(reply)) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/IMinecraftCommandSender.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | abstract class IMinecraftCommandSender(val user: String, val env: IBridgeCommand.CommandEnvironment, val op: Boolean) { 4 | /** 5 | * @param cmdString The command to execute with its arguments 6 | * 7 | * @return True for success, or false for failure 8 | */ 9 | abstract fun execute(cmdString: String): Boolean 10 | 11 | val displayName = env.username ?: user 12 | val accountName = when (env) { 13 | is IBridgeCommand.CommandEnvironment.BridgeEnv -> "$user (id=${env.userId} platform=${env.platform}${env.uuid?.let { " uuid=$it" } 14 | ?: ""}${env.username?.let { " username=$it" } ?: ""})" 15 | is IBridgeCommand.CommandEnvironment.GameEnv -> "$user (username=${env.username} uuid=${env.uuid})" 16 | } 17 | 18 | fun canExecute(commandName: String): Boolean { 19 | if (op) return true 20 | val command = BridgeCommandRegistry[commandName] ?: return false 21 | return command.canExecute(env.uuid) 22 | } 23 | 24 | private var finished = true 25 | val reply = mutableListOf() 26 | 27 | /** 28 | * accumulates response 29 | */ 30 | fun appendReply(text: String) { 31 | if (finished) { 32 | reply.clear() 33 | finished = false 34 | } 35 | reply += text 36 | } 37 | 38 | suspend fun sendReply(cmdString: String) { 39 | env.respond( 40 | text = reply.joinToString("\n"), 41 | cause = "executed command: $cmdString" 42 | ) 43 | finished = true 44 | } 45 | } -------------------------------------------------------------------------------- /1.7.10/src/main/kotlin/matterlink/command/MatterLinkCommandSender.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import matterlink.bridge.command.IBridgeCommand 5 | import matterlink.bridge.command.IMinecraftCommandSender 6 | import net.minecraft.command.ICommandSender 7 | import net.minecraft.server.MinecraftServer 8 | import net.minecraft.util.ChatComponentText 9 | import net.minecraft.util.ChunkCoordinates 10 | import net.minecraft.util.IChatComponent 11 | import net.minecraft.world.World 12 | 13 | class MatterLinkCommandSender( 14 | user: String, 15 | env: IBridgeCommand.CommandEnvironment, 16 | op: Boolean 17 | ) : IMinecraftCommandSender(user, env, op), ICommandSender { 18 | 19 | override fun execute(cmdString: String): Boolean = runBlocking { 20 | return@runBlocking 0 < MinecraftServer.getServer().commandManager.executeCommand( 21 | this@MatterLinkCommandSender, 22 | cmdString 23 | ).apply { 24 | sendReply(cmdString) 25 | } 26 | } 27 | 28 | override fun getFormattedCommandSenderName(): IChatComponent { 29 | return ChatComponentText(displayName) 30 | } 31 | 32 | override fun getCommandSenderName() = accountName 33 | 34 | override fun getEntityWorld(): World { 35 | return MinecraftServer.getServer().worldServerForDimension(0) 36 | } 37 | 38 | override fun canCommandSenderUseCommand(permLevel: Int, commandName: String): Boolean { 39 | //we do permission 40 | return canExecute(commandName) 41 | } 42 | 43 | override fun addChatMessage(component: IChatComponent) { 44 | appendReply(component.unformattedText) 45 | } 46 | 47 | override fun getCommandSenderPosition(): ChunkCoordinates = ChunkCoordinates(0, 0, 0) 48 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/JoinLeaveHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.antiping 4 | import matterlink.config.cfg 5 | import matterlink.mapFormat 6 | import matterlink.stripColorOut 7 | 8 | object JoinLeaveHandler { 9 | suspend fun handleJoin( 10 | player: String, 11 | x: Int, y: Int, z: Int, 12 | dimension: Int 13 | ) { 14 | if (cfg.outgoing.joinPart.enable) { 15 | val msg = cfg.outgoing.joinPart.joinServer.mapFormat( 16 | mapOf( 17 | "{username}" to player.stripColorOut, 18 | "{username:antiping}" to player.stripColorOut.antiping 19 | ) 20 | ) 21 | LocationHandler.sendToLocations( 22 | msg = msg, 23 | x = x, y = y, z = z, dimension = dimension, 24 | event = ChatEvent.JOIN, 25 | systemuser = true, 26 | cause = "$player joined" 27 | ) 28 | } 29 | } 30 | 31 | suspend fun handleLeave( 32 | player: String, 33 | x: Int, y: Int, z: Int, 34 | dimension: Int 35 | ) { 36 | if (cfg.outgoing.joinPart.enable) { 37 | val msg = cfg.outgoing.joinPart.partServer.mapFormat( 38 | mapOf( 39 | "{username}" to player.stripColorOut, 40 | "{username:antiping}" to player.stripColorOut.antiping 41 | ) 42 | ) 43 | LocationHandler.sendToLocations( 44 | msg = msg, 45 | x = x, y = y, z = z, dimension = dimension, 46 | event = ChatEvent.JOIN, 47 | systemuser = true, 48 | cause = "$player left" 49 | ) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/RequestPermissionsCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.config.PermissionConfig 4 | import matterlink.config.PermissionRequest 5 | import matterlink.config.cfg 6 | import matterlink.randomString 7 | 8 | object RequestPermissionsCommand : IBridgeCommand() { 9 | val syntax = " Syntax: request [permissionLevel]" 10 | override val help: String = "Requests permissions on the bridge. $syntax" 11 | override val permLevel: Double 12 | get() = cfg.command.defaultPermAuthenticated 13 | 14 | override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean { 15 | 16 | val uuid = env.uuid 17 | if (uuid == null) { 18 | env.respond("$user is not authenticated ($env)") 19 | return true 20 | } 21 | 22 | val argList = args.split(' ', limit = 2) 23 | val requestedLevelArg = argList.getOrNull(0) 24 | val requestedLevel = requestedLevelArg?.takeIf { it.isNotEmpty() }?.let { 25 | it.toDoubleOrNull() ?: run { 26 | env.respond( 27 | "cannot parse permlevel '$requestedLevelArg'\n" + 28 | syntax 29 | ) 30 | return true 31 | } 32 | } 33 | 34 | val nonce = randomString(length = 3).toUpperCase() 35 | 36 | val requestId = user.toLowerCase() 37 | 38 | PermissionConfig.permissionRequests.put( 39 | requestId, 40 | PermissionRequest(uuid = uuid, user = user, nonce = nonce, powerlevel = requestedLevel) 41 | ) 42 | env.respond("please ask a op to accept your permission elevation with `/ml permAccept $requestId $nonce [permLevel]`") 43 | 44 | return true 45 | } 46 | } -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/command/MatterLinkCommandSender.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import matterlink.bridge.command.IBridgeCommand 5 | import matterlink.bridge.command.IMinecraftCommandSender 6 | import net.minecraft.command.ICommandSender 7 | import net.minecraft.server.MinecraftServer 8 | import net.minecraft.util.text.ITextComponent 9 | import net.minecraft.util.text.TextComponentString 10 | import net.minecraft.world.World 11 | import net.minecraftforge.fml.common.FMLCommonHandler 12 | import javax.annotation.Nonnull 13 | 14 | class MatterLinkCommandSender( 15 | user: String, 16 | env: IBridgeCommand.CommandEnvironment, 17 | op: Boolean 18 | ) : IMinecraftCommandSender(user, env, op), ICommandSender { 19 | 20 | override fun execute(cmdString: String): Boolean = runBlocking { 21 | 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand( 22 | this@MatterLinkCommandSender, 23 | cmdString 24 | ).apply { 25 | sendReply(cmdString) 26 | } 27 | } 28 | 29 | override fun getDisplayName(): ITextComponent { 30 | return TextComponentString(displayName) 31 | } 32 | 33 | override fun getName() = accountName 34 | 35 | override fun getEntityWorld(): World { 36 | return FMLCommonHandler.instance().minecraftServerInstance.getWorld(0) 37 | } 38 | 39 | override fun canUseCommand(permLevel: Int, commandName: String): Boolean { 40 | //permissions are checked on our end 41 | return canExecute(commandName) 42 | } 43 | 44 | override fun getServer(): MinecraftServer? { 45 | return FMLCommonHandler.instance().minecraftServerInstance 46 | } 47 | 48 | override fun sendMessage(@Nonnull component: ITextComponent?) { 49 | appendReply(component!!.unformattedComponentText) 50 | } 51 | 52 | override fun sendCommandFeedback(): Boolean { 53 | return true 54 | } 55 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/gradle,kotlin,intellij+iml 2 | 3 | ### Intellij ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff: 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/dictionaries 11 | 12 | # Sensitive or high-churn files: 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.xml 16 | .idea/**/dataSources.local.xml 17 | .idea/**/sqlDataSources.xml 18 | .idea/**/dynamic.xml 19 | .idea/**/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | ## File-based project format: 26 | *.iws 27 | 28 | ## Plugin-specific files: 29 | 30 | # IntelliJ 31 | out/ 32 | 33 | # Crashlytics plugin (for Android Studio and IntelliJ) 34 | com_crashlytics_export_strings.xml 35 | crashlytics.properties 36 | crashlytics-build.properties 37 | fabric.properties 38 | 39 | ### Intellij+all Patch ### 40 | # Ignores the whole idea folder 41 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 42 | 43 | .idea/ 44 | 45 | ### Intellij+iml Patch ### 46 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 47 | 48 | *.iml 49 | modules.xml 50 | .idea/misc.xml 51 | *.ipr 52 | 53 | ### Kotlin ### 54 | # Compiled class file 55 | *.class 56 | 57 | # Log file 58 | *.log 59 | 60 | # BlueJ files 61 | *.ctxt 62 | 63 | # Mobile Tools for Java (J2ME) 64 | .mtj.tmp/ 65 | 66 | # Package Files # 67 | *.jar 68 | *.war 69 | *.ear 70 | *.zip 71 | *.tar.gz 72 | *.rar 73 | 74 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 75 | hs_err_pid* 76 | 77 | ### Gradle ### 78 | .gradle 79 | **/build/ 80 | 81 | # Ignore Gradle GUI config 82 | gradle-app.setting 83 | 84 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 85 | !gradle-wrapper.jar 86 | 87 | # Cache of project 88 | .gradletasknamecache 89 | 90 | # End of https://www.gitignore.io/api/gradle,kotlin,intellij+iml 91 | 92 | ### MatterLink ### 93 | 94 | classes/ 95 | run/ 96 | *.tmp 97 | **/gen/ 98 | 99 | \.floo 100 | \.flooignore 101 | 102 | bugreport/ 103 | -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/command/MatterLinkCommandSender.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import matterlink.bridge.command.IBridgeCommand 5 | import matterlink.bridge.command.IMinecraftCommandSender 6 | import net.minecraft.command.CommandResultStats 7 | import net.minecraft.command.ICommandSender 8 | import net.minecraft.entity.Entity 9 | import net.minecraft.server.MinecraftServer 10 | import net.minecraft.util.math.BlockPos 11 | import net.minecraft.util.math.Vec3d 12 | import net.minecraft.util.text.ITextComponent 13 | import net.minecraft.util.text.TextComponentString 14 | import net.minecraft.world.World 15 | import net.minecraftforge.fml.common.FMLCommonHandler 16 | import javax.annotation.Nonnull 17 | 18 | class MatterLinkCommandSender( 19 | user: String, 20 | env: IBridgeCommand.CommandEnvironment, 21 | op: Boolean 22 | ) : IMinecraftCommandSender(user, env, op), ICommandSender { 23 | override fun execute(cmdString: String): Boolean = runBlocking { 24 | return@runBlocking 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand( 25 | this@MatterLinkCommandSender, 26 | cmdString 27 | ).apply { 28 | sendReply(cmdString) 29 | } 30 | } 31 | 32 | override fun getDisplayName(): ITextComponent { 33 | return TextComponentString(displayName) 34 | } 35 | 36 | override fun getName() = accountName 37 | 38 | override fun getEntityWorld(): World { 39 | return FMLCommonHandler.instance().minecraftServerInstance.worldServerForDimension(0) 40 | } 41 | 42 | override fun canCommandSenderUseCommand(permLevel: Int, commandName: String): Boolean { 43 | //we check user on our end 44 | return canExecute(commandName) 45 | } 46 | 47 | override fun getServer(): MinecraftServer? { 48 | return FMLCommonHandler.instance().minecraftServerInstance 49 | } 50 | 51 | override fun addChatMessage(@Nonnull component: ITextComponent?) { 52 | appendReply(component!!.unformattedComponentText) 53 | } 54 | 55 | override fun sendCommandFeedback(): Boolean { 56 | return true 57 | } 58 | 59 | override fun getPosition(): BlockPos = BlockPos.ORIGIN 60 | 61 | override fun setCommandStat(type: CommandResultStats.Type?, amount: Int) {} 62 | 63 | override fun getPositionVector(): Vec3d = Vec3d.ZERO 64 | 65 | override fun getCommandSenderEntity(): Entity? = null 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/IMatterLink.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import matterlink.bridge.MessageHandlerInst 4 | import matterlink.bridge.command.BridgeCommandRegistry 5 | import matterlink.bridge.command.IBridgeCommand 6 | import matterlink.bridge.command.IMinecraftCommandSender 7 | import matterlink.config.cfg 8 | import matterlink.update.UpdateChecker 9 | import java.util.UUID 10 | 11 | lateinit var logger: Logger 12 | 13 | lateinit var instance: IMatterLink 14 | 15 | abstract class IMatterLink { 16 | abstract val mcVersion: String 17 | abstract val modVersion: String 18 | abstract val buildNumber: Int 19 | abstract val forgeVersion: String 20 | 21 | abstract fun commandSenderFor( 22 | user: String, 23 | env: IBridgeCommand.CommandEnvironment, 24 | op: Boolean 25 | ): IMinecraftCommandSender 26 | 27 | abstract fun wrappedSendToPlayers(msg: String) 28 | 29 | abstract fun wrappedSendToPlayer(username: String, msg: String) 30 | abstract fun wrappedSendToPlayer(uuid: UUID, msg: String) 31 | abstract fun isOnline(username: String): Boolean 32 | abstract fun nameToUUID(username: String): UUID? 33 | abstract fun uuidToName(uuid: UUID): String? 34 | 35 | suspend fun start() { 36 | MessageHandlerInst.logger = logger 37 | serverStartTime = System.currentTimeMillis() 38 | 39 | if (cfg.connect.autoConnect) 40 | MessageHandlerInst.start("Server started, connecting to matterbridge API", true) 41 | UpdateChecker.check() 42 | } 43 | 44 | suspend fun stop() { 45 | MessageHandlerInst.stop("Server shutting down, disconnecting from matterbridge API") 46 | } 47 | 48 | /** 49 | * in milliseconds 50 | */ 51 | var serverStartTime: Long = System.currentTimeMillis() 52 | 53 | fun getUptimeAsString(): String { 54 | val total = (System.currentTimeMillis() - serverStartTime) / 1000 55 | val s = total % 60 56 | val m = (total / 60) % 60 57 | val h = (total / 3600) % 24 58 | val d = total / 86400 59 | 60 | fun timeFormat(unit: Long, name: String) = when { 61 | unit > 1L -> "$unit ${name}s " 62 | unit == 1L -> "$unit $name " 63 | else -> "" 64 | } 65 | 66 | var result = "" 67 | result += timeFormat(d, "Day") 68 | result += timeFormat(h, "Hour") 69 | result += timeFormat(m, "Minute") 70 | result += timeFormat(s, "Second") 71 | return result 72 | } 73 | 74 | fun registerBridgeCommands() { 75 | BridgeCommandRegistry.reloadCommands() 76 | } 77 | 78 | abstract fun collectPlayers(area: Area): Set 79 | 80 | } -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath group: "org.jetbrains.kotlin", name: "kotlin-gradle-plugin", version: kotlinVersion 8 | classpath group: "com.github.jengelman.gradle.plugins", name: "shadow", version: shadowVersion 9 | classpath group: 'com.vanniktech', name: 'gradle-dependency-graph-generator-plugin', version: '0.5.0' 10 | } 11 | } 12 | 13 | apply plugin: 'kotlin' 14 | apply plugin: "com.github.johnrengelman.shadow" 15 | apply plugin: "com.vanniktech.dependency.graph.generator" 16 | 17 | sourceCompatibility = targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. 18 | 19 | dependencies { 20 | compile project(":Jankson") 21 | shadow(project(':Jankson')) { transitive = false } 22 | 23 | compile group: 'com.google.guava', name: 'guava', version: '+' 24 | 25 | compile(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion) 26 | shadow(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion) { transitive = true } 27 | compile(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion) 28 | shadow(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion) { transitive = true } 29 | compile(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion) 30 | shadow(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion) { transitive = true } 31 | // compile(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion) 32 | // shadow(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion) { transitive = false } 33 | 34 | // compile(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion) 35 | // shadow(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion) { transitive = false } 36 | 37 | compile(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion) 38 | shadow(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion) { transitive = false } 39 | 40 | compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion 41 | } 42 | 43 | compileKotlin { 44 | kotlinOptions { 45 | jvmTarget = "1.8" 46 | } 47 | } 48 | 49 | shadowJar { 50 | classifier = '' 51 | 52 | relocate 'blue.endless', 'matterlink.repack.blue.endless' 53 | relocate 'com.github', 'matterlink.repack.com.github' 54 | relocate 'kotlinx', 'matterlink.repack.kotlinx' 55 | configurations = [project.configurations.shadow] 56 | } 57 | 58 | //tasks.build.dependsOn shadowJar -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/MessageHandlerInst.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge 2 | 3 | import matterlink.Paste 4 | import matterlink.PasteSection 5 | import matterlink.PasteUtil 6 | import matterlink.antiping 7 | import matterlink.api.ApiMessage 8 | import matterlink.api.MessageHandler 9 | import matterlink.config.cfg 10 | import matterlink.handlers.ChatEvent 11 | import matterlink.handlers.LocationHandler 12 | import matterlink.mapFormat 13 | import matterlink.stackTraceString 14 | 15 | object MessageHandlerInst : MessageHandler() { 16 | override suspend fun transmit(msg: ApiMessage) { 17 | transmit(msg, cause = "") 18 | } 19 | 20 | override suspend fun sendStatusUpdate(message: String) { 21 | LocationHandler.sendToLocations( 22 | msg = message, 23 | x = 0, y = 0, z = 0, dimension = null, 24 | systemuser = true, 25 | event = ChatEvent.STATUS, 26 | cause = "status update message" 27 | ) 28 | } 29 | 30 | suspend fun transmit(msg: ApiMessage, cause: String, maxLines: Int = cfg.outgoing.inlineLimit) { 31 | if (msg.username.isEmpty()) { 32 | msg.username = cfg.outgoing.systemUser 33 | 34 | if (msg.avatar.isEmpty() && cfg.outgoing.avatar.enable) { 35 | msg.avatar = cfg.outgoing.avatar.systemUserAvatar 36 | } 37 | } 38 | if (msg.gateway.isEmpty()) { 39 | logger.error("dropped message '$msg' due to missing gateway") 40 | return 41 | } 42 | 43 | if (msg.text.lines().count() >= maxLines) { 44 | try { 45 | val response = PasteUtil.paste( 46 | Paste( 47 | description = cause, 48 | sections = listOf( 49 | PasteSection( 50 | name = "log.txt", 51 | syntax = "text", 52 | contents = msg.text 53 | ) 54 | ) 55 | ) 56 | ) 57 | msg.text = msg.text.substringBefore('\n') 58 | .take(25) + "... " + response.link 59 | } catch (e: Exception) { 60 | logger.error(cause) 61 | logger.error(e.stackTraceString) 62 | } 63 | } 64 | super.transmit(msg) 65 | } 66 | } 67 | 68 | fun ApiMessage.format(fmt: String): String { 69 | return fmt.mapFormat( 70 | mapOf( 71 | "{username}" to username, 72 | "{text}" to text, 73 | "{gateway}" to gateway, 74 | "{channel}" to channel, 75 | "{protocol}" to protocol, 76 | "{username:antiping}" to username.antiping 77 | ) 78 | ) 79 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/command/CommandCoreML.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import matterlink.bridge.MessageHandlerInst 4 | import matterlink.bridge.command.BridgeCommandRegistry 5 | import matterlink.config.PermissionConfig 6 | import matterlink.config.baseCfg 7 | import matterlink.config.cfg 8 | 9 | object CommandCoreML { 10 | val name = "ml" 11 | 12 | val aliases = listOf("matterlink") 13 | 14 | val usage = "ml " 15 | 16 | suspend fun execute(args: Array, user: String, uuid: String?): String { 17 | val cmd = args[0].toLowerCase() 18 | 19 | return when (cmd) { 20 | "connect" -> { 21 | MessageHandlerInst.start("Bridge connected by console", true) 22 | "Attempting bridge connection!" 23 | } 24 | "disconnect" -> { 25 | MessageHandlerInst.stop("Bridge disconnected by console") 26 | "Bridge disconnected!" 27 | } 28 | "reload" -> { 29 | // if (MessageHandlerInst.connected) 30 | MessageHandlerInst.stop("Bridge restarting (reload command issued by console)") 31 | cfg = baseCfg.load() 32 | BridgeCommandRegistry.reloadCommands() 33 | // if (!MessageHandlerInst.connected) 34 | MessageHandlerInst.start("Bridge reconnected", false) 35 | "Bridge config reloaded!" 36 | } 37 | "permaccept" -> { 38 | val requestId = args.getOrNull(1)?.toLowerCase() ?: run { 39 | return "no requestId passed" 40 | } 41 | val request = PermissionConfig.permissionRequests.getIfPresent(requestId.toLowerCase()) ?: run { 42 | return "No request available" 43 | } 44 | val nonce = args.getOrNull(2)?.toUpperCase() ?: run { 45 | return "no code passed" 46 | } 47 | if (request.nonce != nonce) { 48 | return "nonce in request does not match with $nonce" 49 | } 50 | val powerLevelArg = args.getOrNull(3)?.toDoubleOrNull() 51 | val powerLevel = powerLevelArg ?: run { return "permLevel cannot be parsed: ${args.getOrNull(3)}" } 52 | ?: request.powerlevel 53 | ?: return "no permLevel provided" 54 | PermissionConfig.add(request.uuid, powerLevel, "${request.user} Authorized by $user") 55 | PermissionConfig.permissionRequests.invalidate(requestId) 56 | "added ${request.user} (uuid: ${request.uuid}) with power level: $powerLevel" 57 | } 58 | else -> { 59 | "Invalid arguments for command! \n" + 60 | "usage: ${CommandCoreAuth.usage}" 61 | } 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/config/PermissionConfig.kt: -------------------------------------------------------------------------------- 1 | package matterlink.config 2 | 3 | import blue.endless.jankson.Jankson 4 | import blue.endless.jankson.JsonObject 5 | import blue.endless.jankson.impl.SyntaxError 6 | import com.google.common.cache.Cache 7 | import com.google.common.cache.CacheBuilder 8 | import matterlink.getReified 9 | import matterlink.logger 10 | import java.io.File 11 | import java.io.FileNotFoundException 12 | import java.util.UUID 13 | import java.util.concurrent.TimeUnit 14 | 15 | typealias PermissionMap = Map 16 | 17 | data class PermissionRequest( 18 | val uuid: UUID, 19 | val user: String, 20 | val nonce: String, 21 | val powerlevel: Double? = null 22 | ) 23 | 24 | object PermissionConfig { 25 | val permissionRequests: Cache = CacheBuilder.newBuilder() 26 | .expireAfterWrite(10, TimeUnit.MINUTES) 27 | .build() 28 | private val jankson = Jankson 29 | .builder() 30 | .build() 31 | 32 | private val configFile: File = baseCfg.cfgDirectory.resolve("permissions.hjson") 33 | 34 | private val default = mapOf( 35 | "edd31c45-b095-49c5-a9f5-59cec4cfed8c" to 9000.0 to "Superuser" 36 | ) 37 | 38 | val perms: PermissionMap = mutableMapOf() 39 | private var jsonObject: JsonObject = JsonObject() 40 | 41 | fun loadFile() { 42 | val defaultJsonObject = JsonObject().apply { 43 | default.forEach { (uuid, level), comment -> 44 | jsonObject.putDefault(uuid, level, comment) 45 | } 46 | } 47 | 48 | var save = true 49 | jsonObject = try { 50 | jankson.load(configFile) 51 | } catch (e: SyntaxError) { 52 | logger.error("error parsing config: ${e.completeMessage}") 53 | save = false 54 | defaultJsonObject 55 | } catch (e: FileNotFoundException) { 56 | logger.error("cannot find config: $configFile .. creating sample permissions mapping") 57 | configFile.createNewFile() 58 | defaultJsonObject 59 | } 60 | 61 | load(save) 62 | } 63 | 64 | private fun load(save: Boolean = true) { 65 | val tmpPerms = mutableMapOf() 66 | for ((uuid, powerlevel) in jsonObject) { 67 | val tmpLevel = jsonObject.getReified(uuid) 68 | if (tmpLevel == null) { 69 | logger.warn("cannot parse permission uuid: $uuid level: $powerlevel") 70 | continue 71 | } 72 | tmpPerms[uuid] = tmpLevel 73 | } 74 | 75 | logger.info("Permissions reloaded") 76 | 77 | if (save) 78 | configFile.writeText(jsonObject.toJson(true, true)) 79 | } 80 | 81 | fun add(uuid: UUID, powerlevel: Double, comment: String? = null) { 82 | jsonObject.putDefault(uuid.toString(), powerlevel, comment) 83 | load() 84 | } 85 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/IBridgeCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.api.ApiMessage 4 | import matterlink.bridge.MessageHandlerInst 5 | import matterlink.config.PermissionConfig 6 | import matterlink.config.cfg 7 | import matterlink.handlers.TickHandler 8 | import matterlink.instance 9 | import matterlink.logger 10 | import matterlink.stripColorOut 11 | import java.util.UUID 12 | 13 | abstract class IBridgeCommand { 14 | abstract val help: String 15 | abstract val permLevel: Double 16 | open val timeout: Int = 20 17 | 18 | sealed class CommandEnvironment { 19 | abstract val uuid: UUID? 20 | abstract val username: String? 21 | 22 | data class BridgeEnv( 23 | val name: String, 24 | val userId: String, 25 | val platform: String, 26 | val gateway: String, 27 | override val uuid: UUID? 28 | ) : CommandEnvironment() { 29 | override val username: String? 30 | get() = uuid?.let { instance.uuidToName(uuid) } 31 | } 32 | 33 | data class GameEnv( 34 | override val username: String, 35 | override val uuid: UUID 36 | ) : CommandEnvironment() 37 | 38 | suspend fun respond(text: String, cause: String = "") { 39 | when (this) { 40 | is BridgeEnv -> { 41 | MessageHandlerInst.transmit( 42 | ApiMessage( 43 | gateway = this.gateway, 44 | text = text.stripColorOut 45 | ), 46 | cause = cause 47 | ) 48 | } 49 | is GameEnv -> { 50 | instance.wrappedSendToPlayer(uuid, text) 51 | } 52 | } 53 | 54 | } 55 | } 56 | 57 | 58 | private var lastUsed: Int = 0 59 | 60 | val alias: String 61 | get() = BridgeCommandRegistry.getName(this)!! 62 | 63 | fun reachedTimeout(): Boolean { 64 | return (TickHandler.tickCounter - lastUsed > timeout) 65 | } 66 | 67 | fun preExecute() { 68 | lastUsed = TickHandler.tickCounter 69 | } 70 | 71 | /** 72 | * 73 | * @return consume message flag 74 | */ 75 | abstract suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean 76 | 77 | fun canExecute(uuid: UUID?): Boolean { 78 | logger.trace("canExecute this: $this uuid: $uuid permLevel: $permLevel") 79 | val canExec = getPermLevel(uuid) >= permLevel 80 | logger.trace("canExecute return $canExec") 81 | return canExec 82 | } 83 | 84 | open fun validate() = true 85 | 86 | companion object { 87 | fun getPermLevel(uuid: UUID?): Double { 88 | if (uuid == null) return cfg.command.defaultPermUnauthenticated 89 | return PermissionConfig.perms[uuid.toString()] ?: cfg.command.defaultPermAuthenticated 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/AuthBridgeCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.config.AuthRequest 4 | import matterlink.config.IdentitiesConfig 5 | import matterlink.config.cfg 6 | import matterlink.instance 7 | import matterlink.randomString 8 | import java.util.UUID 9 | 10 | object AuthBridgeCommand : IBridgeCommand() { 11 | val syntax = "Syntax: auth [username]" 12 | override val help: String = "Requests authentication on the bridge. $syntax" 13 | override val permLevel: Double 14 | get() = cfg.command.defaultPermUnauthenticated 15 | 16 | override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean { 17 | if (env !is CommandEnvironment.BridgeEnv) { 18 | env.respond("please initiate authentication from linked external chat") 19 | return true 20 | } 21 | 22 | val uuid = env.uuid 23 | if (uuid != null) { 24 | val name = instance.uuidToName(uuid) 25 | env.respond("you are already authenticated as name: $name uuid: $uuid") 26 | return true 27 | } 28 | 29 | val argList = args.split(' ', limit = 2) 30 | val target = argList.getOrNull(0) ?: run { 31 | env.respond( 32 | "no username/uuid provided\n" + 33 | syntax 34 | ) 35 | 36 | return true 37 | } 38 | 39 | var targetUserName = target 40 | 41 | val targetUUid: String = instance.nameToUUID(target)?.toString() ?: run { 42 | try { 43 | targetUserName = instance.uuidToName(UUID.fromString(target)) ?: run { 44 | env.respond("cannot find player by username/uuid $target") 45 | return true 46 | } 47 | } catch (e: IllegalArgumentException) { 48 | env.respond("invalid username/uuid $target") 49 | return true 50 | } 51 | target 52 | } 53 | 54 | val online = instance.isOnline(targetUserName) 55 | if (!online) { 56 | env.respond("$targetUserName is not online, please log in and try again to send instructions") 57 | return true 58 | } 59 | val nonce = randomString(length = 3).toUpperCase() 60 | 61 | val requestId = user.toLowerCase() 62 | instance.wrappedSendToPlayer(targetUserName, "have you requested authentication with the MatterLink system?") 63 | instance.wrappedSendToPlayer(targetUserName, "if yes please execute /auth accept $user $nonce") 64 | instance.wrappedSendToPlayer(targetUserName, "otherwise you may ignore this message") 65 | 66 | 67 | IdentitiesConfig.authRequests.put( 68 | requestId, 69 | AuthRequest( 70 | username = targetUserName, 71 | uuid = targetUUid, 72 | nonce = nonce, 73 | platform = env.platform, 74 | userid = env.userId 75 | ) 76 | ) 77 | env.respond("please accept the authentication request ingame, do not share the code") 78 | 79 | return true 80 | } 81 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/command/CommandCoreAuth.kt: -------------------------------------------------------------------------------- 1 | package matterlink.command 2 | 3 | import matterlink.config.IdentitiesConfig 4 | 5 | object CommandCoreAuth { 6 | val name = "auth" 7 | 8 | val aliases = listOf("authenticate") 9 | 10 | val usage = "auth " 11 | 12 | fun execute(args: Array, user: String, uuid: String?): String { 13 | val cmd = args[0].toLowerCase() 14 | 15 | return when (cmd) { 16 | "accept" -> { 17 | val requestId = args.getOrNull(1)?.toLowerCase() ?: run { 18 | return "no requestId passed" 19 | } 20 | val request = IdentitiesConfig.authRequests.getIfPresent(requestId.toLowerCase()) ?: run { 21 | return "No request available" 22 | } 23 | val nonce = args.getOrNull(2)?.toUpperCase() ?: run { 24 | return "no code passed" 25 | } 26 | if (request.nonce != nonce) { 27 | return "nonce in request does not match" 28 | } 29 | if (request.username != user) { 30 | return "username in request does not match ${request.username} != $user" 31 | } 32 | if (request.uuid != uuid) { 33 | return "uuid in request does not match ${request.uuid} != $uuid" 34 | } 35 | 36 | IdentitiesConfig.add( 37 | request.uuid, 38 | request.username, 39 | request.platform, 40 | request.userid, 41 | "Accepted by $user" 42 | ) 43 | 44 | IdentitiesConfig.authRequests.invalidate(requestId) 45 | "${request.userid} on ${request.platform} is now identified as $user" 46 | } 47 | "reject" -> { 48 | 49 | val requestId = args.getOrNull(1)?.toLowerCase() ?: run { 50 | return "no requestId passed" 51 | } 52 | val request = IdentitiesConfig.authRequests.getIfPresent(requestId.toLowerCase()) ?: run { 53 | return "No request available" 54 | } 55 | val nonce = args.getOrNull(2)?.toUpperCase() ?: run { 56 | return "no code passed" 57 | } 58 | if (request.nonce != nonce) { 59 | return "nonce in request does not match" 60 | } 61 | if (request.username != user) { 62 | return "username in request does not match ${request.username} != $user" 63 | } 64 | if (request.uuid != uuid) { 65 | return "uuid in request does not match ${request.uuid} != $uuid" 66 | } 67 | 68 | IdentitiesConfig.authRequests.invalidate(requestId) 69 | "request $nonce for ${request.userid} on ${request.platform} was invalidated" 70 | } 71 | else -> { 72 | "Invalid arguments for command! \n" + 73 | "usage: $usage" 74 | } 75 | } 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/PasteUtil.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import blue.endless.jankson.Jankson 4 | import java.net.HttpURLConnection 5 | import java.net.URL 6 | 7 | 8 | /** 9 | * Created by nikky on 09/07/18. 10 | * @author Nikky 11 | */ 12 | 13 | data class Paste( 14 | val encrypted: Boolean = false, 15 | val description: String, 16 | val sections: List 17 | ) 18 | 19 | data class PasteSection( 20 | val name: String, 21 | val syntax: String = "text", 22 | val contents: String 23 | ) 24 | 25 | data class PasteResponse( 26 | val id: String, 27 | val link: String 28 | ) 29 | 30 | object PasteUtil { 31 | private const val DEFAULT_KEY = "uKJoyicVJFnmpnrIZMklOURWxrCKXYaiBWOzPmvon" 32 | 33 | private val jankson = Jankson.builder() 34 | .registerTypeAdapter { 35 | PasteResponse( 36 | id = it.getReified("id") ?: "", 37 | link = it.getReified("link") 38 | ?.replace("\\/", "/") 39 | ?: "invalid" 40 | ) 41 | } 42 | // .registerSerializer { paste: Paste, marshaller: Marshaller -> 43 | // JsonObject().apply { 44 | // with(paste) { 45 | // if (description.isNotBlank()) 46 | // this@apply["description"] = marshaller.serialize(description) 47 | // if (encrypted) 48 | // this@apply["encrypted"] = marshaller.serialize(encrypted) 49 | // this@apply["sections"] = marshaller.serialize(sections) 50 | // } 51 | // } 52 | // } 53 | // .registerSerializer { section: PasteSection, marshaller: Marshaller -> 54 | // JsonObject().apply { 55 | // with(section) { 56 | // if (name.isNotBlank()) 57 | // this@apply["name"] = marshaller.serialize(name) 58 | // this@apply["syntax"] = marshaller.serialize(syntax) 59 | // this@apply["contents"] = marshaller.serialize(contents.replace("\n", "\\n")) 60 | // } 61 | // } 62 | // } 63 | .build() 64 | 65 | fun paste(paste: Paste, key: String = ""): PasteResponse { 66 | val apiKey = key.takeIf { it.isNotBlank() } ?: DEFAULT_KEY 67 | 68 | val url = URL("https://api.paste.ee/v1/pastes") 69 | val http = url.openConnection() as HttpURLConnection 70 | http.requestMethod = "POST" 71 | http.doOutput = true 72 | 73 | val out = jankson.toJson(paste) 74 | .toJson(false, false) 75 | .toByteArray() 76 | 77 | http.setFixedLengthStreamingMode(out.size) 78 | http.setRequestProperty("Content-Type", "application/json; charset=UTF-8") 79 | http.setRequestProperty("X-Auth-Token", apiKey) 80 | http.connect() 81 | http.outputStream.use { os -> 82 | os.write(out) 83 | } 84 | 85 | val textResponse = http.inputStream.bufferedReader().use { it.readText() } 86 | logger.debug("response: $textResponse") 87 | // val jsonObject = jankson.load(http.inputStream) 88 | return jankson.fromJson(textResponse) 89 | } 90 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/api/ApiMessage.kt: -------------------------------------------------------------------------------- 1 | package matterlink.api 2 | 3 | import kotlinx.serialization.Encoder 4 | import kotlinx.serialization.Optional 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.Serializer 7 | import kotlinx.serialization.json.JSON 8 | 9 | /** 10 | * Created by nikky on 07/05/18. 11 | * 12 | * @author Nikky 13 | * @version 1.0 14 | */ 15 | @Serializable 16 | data class ApiMessage( 17 | @Optional var username: String = "", 18 | @Optional var text: String = "", 19 | @Optional var gateway: String = "", 20 | @Optional var timestamp: String = "", 21 | @Optional var channel: String = "", 22 | @Optional var userid: String = "", 23 | @Optional var avatar: String = "", 24 | @Optional var account: String = "", 25 | @Optional var protocol: String = "", 26 | @Optional var event: String = "", 27 | @Optional var id: String = "", 28 | @Optional var Extra: Map? = null 29 | ) { 30 | 31 | fun encode(): String { 32 | return JSON.nonstrict.stringify(ApiMessage.serializer(), this) 33 | } 34 | 35 | 36 | override fun toString(): String = encode() 37 | 38 | @Serializer(forClass = ApiMessage::class) 39 | companion object { 40 | val USER_ACTION = "user_action" 41 | val JOIN_LEAVE = "join_leave" 42 | 43 | override fun serialize(output: Encoder, obj: ApiMessage) { 44 | val elemOutput = output.beginStructure(descriptor) 45 | obj.username.takeIf { it.isNotEmpty() }?.let { 46 | elemOutput.encodeStringElement(descriptor, 0, it) 47 | } 48 | obj.text.takeIf { it.isNotEmpty() }?.let { 49 | elemOutput.encodeStringElement(descriptor, 1, it) 50 | } 51 | obj.gateway.takeIf { it.isNotEmpty() }?.let { 52 | elemOutput.encodeStringElement(descriptor, 2, it) 53 | } 54 | obj.timestamp.takeIf { it.isNotEmpty() }?.let { 55 | elemOutput.encodeStringElement(descriptor, 3, it) 56 | } 57 | obj.channel.takeIf { it.isNotEmpty() }?.let { 58 | elemOutput.encodeStringElement(descriptor, 4, it) 59 | } 60 | obj.userid.takeIf { it.isNotEmpty() }?.let { 61 | elemOutput.encodeStringElement(descriptor, 5, it) 62 | } 63 | obj.avatar.takeIf { it.isNotEmpty() }?.let { 64 | elemOutput.encodeStringElement(descriptor, 6, it) 65 | } 66 | obj.account.takeIf { it.isNotEmpty() }?.let { 67 | elemOutput.encodeStringElement(descriptor, 7, it) 68 | } 69 | obj.protocol.takeIf { it.isNotEmpty() }?.let { 70 | elemOutput.encodeStringElement(descriptor, 8, it) 71 | } 72 | obj.event.takeIf { it.isNotEmpty() }?.let { 73 | elemOutput.encodeStringElement(descriptor, 9, it) 74 | } 75 | obj.id.takeIf { it.isNotEmpty() }?.let { 76 | elemOutput.encodeStringElement(descriptor, 10, it) 77 | } 78 | // obj.Extra.takeIf { ! it.isNullOrEmpty() }?.let { 79 | // elemOutput.encodeStringElement(descriptor, 11, it) 80 | // } 81 | elemOutput.endStructure(descriptor) 82 | } 83 | 84 | fun decode(input: String): ApiMessage { 85 | return JSON.nonstrict.parse(ApiMessage.serializer(), input) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/CustomCommand.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.instance 4 | import matterlink.lazyFormat 5 | import matterlink.logger 6 | import matterlink.stripColorIn 7 | 8 | data class CustomCommand( 9 | val type: CommandType = CommandType.RESPONSE, 10 | val execute: String? = null, 11 | val response: String? = null, 12 | override val permLevel: Double = 0.0, 13 | override val help: String = "", 14 | override val timeout: Int = 20, 15 | val defaultCommand: Boolean? = null, 16 | val execOp: Boolean? = null, 17 | val argumentsRegex: Regex? = null 18 | ) : IBridgeCommand() { 19 | 20 | override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean { 21 | if (argumentsRegex != null) { 22 | logger.debug("testing '$args' against '${argumentsRegex.pattern}'") 23 | if (!argumentsRegex.matches(args)) { 24 | env.respond("$user sent invalid input to command $alias") 25 | return false 26 | } 27 | } 28 | 29 | return when (type) { 30 | CommandType.EXECUTE -> { 31 | // uses a new commandsender for each use 32 | val commandSender = instance.commandSenderFor(user, env, execOp ?: false) 33 | val cmd = execute?.lazyFormat(getReplacements(user, env, args))?.stripColorIn 34 | ?: return false 35 | commandSender.execute(cmd) || commandSender.reply.isNotEmpty() 36 | } 37 | CommandType.RESPONSE -> { 38 | env.respond( 39 | response?.lazyFormat(getReplacements(user, env, args)) 40 | ?: "", cause = "response to command: $alias" 41 | ) 42 | 43 | true 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | override fun validate(): Boolean { 52 | val typeCheck = when (type) { 53 | CommandType.EXECUTE -> execute?.isNotBlank() ?: false 54 | CommandType.RESPONSE -> response?.isNotBlank() ?: false 55 | } 56 | if (!typeCheck) return false 57 | 58 | return true 59 | } 60 | 61 | companion object { 62 | val DEFAULT = CustomCommand() 63 | 64 | fun getReplacements(user: String, env: CommandEnvironment, args: String): Map String?> = mapOf( 65 | "{uptime}" to instance::getUptimeAsString, 66 | "{user}" to { user }, 67 | "{userid}" to { 68 | when (env) { 69 | is CommandEnvironment.BridgeEnv -> env.userId 70 | else -> null 71 | } 72 | }, 73 | "{uuid}" to { 74 | when (env) { 75 | is CommandEnvironment.BridgeEnv -> env.uuid.toString() 76 | is CommandEnvironment.GameEnv -> env.uuid.toString() 77 | } 78 | }, 79 | "{username}" to { 80 | when (env) { 81 | is CommandEnvironment.BridgeEnv -> env.uuid 82 | is CommandEnvironment.GameEnv -> env.uuid 83 | }?.let { instance.uuidToName(it) } 84 | }, 85 | "{platform}" to { 86 | when (env) { 87 | is CommandEnvironment.BridgeEnv -> env.platform 88 | else -> null 89 | } 90 | }, 91 | "{args}" to { args }, 92 | "{version}" to { instance.modVersion } 93 | ) 94 | } 95 | } 96 | 97 | enum class CommandType { 98 | EXECUTE, RESPONSE 99 | } -------------------------------------------------------------------------------- /1.12.2/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url = 'http://files.minecraftforge.net/maven' 6 | } 7 | mavenCentral() 8 | maven { 9 | url = 'https://oss.sonatype.org/content/groups/public' 10 | } 11 | maven { 12 | url = 'https://plugins.gradle.org/m2/' 13 | } 14 | } 15 | dependencies { 16 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '2.3-SNAPSHOT' 17 | classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadowVersion 18 | classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion 19 | classpath group: 'com.vanniktech', name: 'gradle-dependency-graph-generator-plugin', version: '0.5.0' 20 | } 21 | } 22 | 23 | apply plugin: 'net.minecraftforge.gradle.forge' 24 | apply plugin: 'com.github.johnrengelman.shadow' 25 | apply plugin: 'com.matthewprenger.cursegradle' 26 | apply plugin: "com.vanniktech.dependency.graph.generator" 27 | 28 | version = project.mc_version + '-' + project.modVersion 29 | 30 | archivesBaseName = project.modName 31 | 32 | sourceCompatibility = targetCompatibility = '1.8' 33 | 34 | dependencies { 35 | compile project(':core') 36 | shadow (project(path: ':core', configuration: 'shadow')) { transitive = false } 37 | 38 | compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelinVersion 39 | } 40 | 41 | shadowJar { 42 | classifier = '' 43 | 44 | exclude 'dummyThing' 45 | configurations = [project.configurations.shadow] 46 | } 47 | 48 | minecraft { 49 | version = project.mc_version + '-' + project.forge_version 50 | runDir = 'run' 51 | 52 | mappings = project.mcp_mappings 53 | 54 | replaceIn 'Constants.kt' 55 | replace '@MODVERSION@', project.modVersion 56 | replace '@MCVERSION@', project.mc_version 57 | replace '@FORGELIN-VERSION@', project.forgelinVersion 58 | replace '@FORGE-VERSION@', project.forge_version 59 | replace '-1//@BUILD_NUMBER@', System.env.BUILD_NUMBER ?: -1 60 | } 61 | 62 | processResources { 63 | // this will ensure that this task is redone when the versions change. 64 | inputs.property "version", project.modVersion 65 | inputs.property "mcversion", project.minecraft.version 66 | 67 | // replace stuff in mcmod.info, nothing else 68 | from(project(":core").sourceSets.main.resources.srcDirs) { 69 | include 'mcmod.info' 70 | 71 | // replace version and mcversion 72 | expand 'version': project.modVersion, 'mcversion': project.minecraft.version 73 | } 74 | 75 | // copy everything else except the mcmod.info 76 | from(project(":core").sourceSets.main.resources.srcDirs) { 77 | exclude 'mcmod.info' 78 | } 79 | 80 | } 81 | 82 | sourceJar { 83 | classifier = 'sources' 84 | // copy all the minecraftforge specific classes 85 | from sourceSets.main.allSource 86 | 87 | // copy everything else except the mcmod.info 88 | from(project(":core").sourceSets.main.allSource) { 89 | exclude 'mcmod.info' 90 | } 91 | } 92 | 93 | reobf { 94 | shadowJar { mappingType = 'SEARGE' } 95 | } 96 | tasks.shadowJar.finalizedBy reobfShadowJar 97 | 98 | curseforge { 99 | if (project.hasProperty('CURSEFORGE_API_TOKEN') && project.hasProperty('release')) { 100 | apiKey = CURSEFORGE_API_TOKEN 101 | } 102 | project { 103 | id = project.curseId 104 | releaseType = project.curseReleaseType 105 | if (project.hasProperty('changelog_file')) { 106 | println("changelog = $changelog_file") 107 | changelogType = 'markdown' 108 | changelog = file(changelog_file) 109 | } 110 | relations { 111 | requiredLibrary 'shadowfacts-forgelin' 112 | } 113 | mainArtifact(shadowJar) { 114 | displayName = "MatterLink $version" 115 | } 116 | } 117 | } 118 | 119 | runServer { 120 | outputs.upToDateWhen { false } 121 | } -------------------------------------------------------------------------------- /1.9.4/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url = 'http://files.minecraftforge.net/maven' 6 | } 7 | mavenCentral() 8 | maven { 9 | url 'https://plugins.gradle.org/m2/' 10 | } 11 | } 12 | dependencies { 13 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '2.2-SNAPSHOT' 14 | classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadowVersion 15 | classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion 16 | } 17 | } 18 | 19 | apply plugin: 'net.minecraftforge.gradle.forge' 20 | apply plugin: 'com.github.johnrengelman.shadow' 21 | apply plugin: 'com.matthewprenger.cursegradle' 22 | 23 | version = project.mc_version + '-' + project.modVersion 24 | 25 | archivesBaseName = project.modName 26 | 27 | sourceCompatibility = targetCompatibility = '1.8' 28 | 29 | dependencies { 30 | compile project(':core') 31 | shadow (project(path: ':core', configuration: 'shadow')) { transitive = false } 32 | 33 | compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelinVersion 34 | } 35 | 36 | shadowJar { 37 | classifier = '' 38 | 39 | exclude 'dummyThing' 40 | configurations = [project.configurations.shadow] 41 | } 42 | 43 | import net.minecraftforge.gradle.user.TaskSourceCopy 44 | 45 | // Mad hacks to make source replacements work for Kotlin 46 | // source: https://github.com/PaleoCrafter/VanillaImmersion/blob/ee82ecafb76659cf7d7822a722c8f63f43f41d01/build.gradle#L119 47 | for (set in sourceSets) { 48 | def taskName = "source${set.name.capitalize()}Kotlin" 49 | def dir = new File(project.getBuildDir(), "sources/${set.name}/kotlin") 50 | task(taskName, type: TaskSourceCopy) { 51 | source = set.getKotlin() 52 | output = dir 53 | } 54 | def compileTask = tasks[set.getCompileTaskName('kotlin')] 55 | compileTask.source = dir 56 | compileTask.dependsOn taskName 57 | def dirPath = dir.toPath() 58 | compileKotlin.include { 59 | return it.file.toPath().startsWith(dirPath) 60 | } 61 | } 62 | sourceJar.from sourceSets.main.kotlin 63 | 64 | minecraft { 65 | version = project.mc_version + '-' + project.forge_version 66 | runDir = 'run' 67 | 68 | mappings = project.mcp_mappings 69 | 70 | replaceIn 'Constants.kt' 71 | replace '@MODVERSION@', project.modVersion 72 | replace '@MCVERSION@', project.mc_version 73 | replace '@FORGELIN-VERSION@', project.forgelinVersion 74 | replace '@FORGE-VERSION@', project.forge_version 75 | replace '-1//@BUILD_NUMBER@', System.env.BUILD_NUMBER ?: -1 76 | } 77 | 78 | processResources { 79 | // this will ensure that this task is redone when the versions change. 80 | inputs.property 'version', project.modVersion 81 | inputs.property 'mcversion', project.minecraft.version 82 | 83 | // replace stuff in mcmod.info, nothing else 84 | from(project(':core').sourceSets.main.resources.srcDirs) { 85 | include 'mcmod.info' 86 | 87 | // replace version and mcversion 88 | expand 'version': project.modVersion, 'mcversion': project.minecraft.version 89 | } 90 | 91 | // copy everything else except the mcmod.info 92 | from(project(':core').sourceSets.main.resources.srcDirs) { 93 | exclude 'mcmod.info' 94 | } 95 | 96 | } 97 | 98 | sourceJar { 99 | classifier 'sources' 100 | // copy all the minecraftforge specific classes 101 | from sourceSets.main.allSource 102 | 103 | // copy everything else except the mcmod.info 104 | from(project(':core').sourceSets.main.allSource) { 105 | exclude 'mcmod.info' 106 | } 107 | } 108 | 109 | 110 | reobf { 111 | shadowJar { mappingType = 'SEARGE' } 112 | } 113 | tasks.shadowJar.finalizedBy reobfShadowJar 114 | 115 | curseforge { 116 | if (project.hasProperty('CURSEFORGE_API_TOKEN') && project.hasProperty('release')) { 117 | apiKey = CURSEFORGE_API_TOKEN 118 | } 119 | project { 120 | id = project.curseId 121 | releaseType = project.curseReleaseType 122 | addGameVersion '1.10' 123 | if (project.hasProperty('changelog_file')) { 124 | println("changelog = $changelog_file") 125 | changelogType = 'markdown' 126 | changelog = file(changelog_file) 127 | } 128 | relations { 129 | requiredLibrary 'shadowfacts-forgelin' 130 | } 131 | mainArtifact(shadowJar) { 132 | displayName = "MatterLink $version" 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/LocationHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import matterlink.api.ApiMessage 4 | import matterlink.bridge.MessageHandlerInst 5 | import matterlink.config.cfg 6 | import matterlink.logger 7 | import matterlink.stripColorOut 8 | import java.util.UUID 9 | 10 | 11 | enum class ChatEvent { 12 | PLAIN, ACTION, DEATH, JOIN, LEAVE, ADVANCEMENT, BROADCAST, STATUS 13 | } 14 | 15 | object LocationHandler { 16 | 17 | suspend fun sendToLocations( 18 | user: String = cfg.outgoing.systemUser, 19 | msg: String, 20 | x: Int, y: Int, z: Int, 21 | dimension: Int?, 22 | event: ChatEvent, 23 | systemuser: Boolean = false, 24 | uuid: UUID? = null, 25 | cause: String 26 | ): Boolean { 27 | val defaults = cfg.outgoingDefaults 28 | var handled = false 29 | val skips = mutableSetOf() 30 | logger.debug("locations: ${cfg.locations.map { it.label }}") 31 | for (location in cfg.locations) { 32 | val label = location.label 33 | if (skips.contains(label)) { 34 | logger.debug("skipping $label (contained in in $skips)") 35 | continue 36 | } 37 | if (!location.area.testForDim(dimension)) { 38 | logger.debug("location: $label dropped message '$msg' from $user due to mismatched dimension") 39 | continue 40 | } 41 | if (!location.area.testInBounds(x, y, z)) { 42 | logger.debug("location: $label dropped message '$msg' from $user out of coordinate bounds") 43 | continue 44 | } 45 | val matchesEvent = when (event) { 46 | ChatEvent.PLAIN -> location.outgoing.plain ?: defaults.plain 47 | ChatEvent.ACTION -> location.outgoing.action ?: defaults.action 48 | ChatEvent.DEATH -> location.outgoing.death ?: defaults.death 49 | ChatEvent.JOIN -> location.outgoing.join ?: defaults.join 50 | ChatEvent.LEAVE -> location.outgoing.leave ?: defaults.leave 51 | ChatEvent.ADVANCEMENT -> location.outgoing.advancement ?: defaults.advancement 52 | ChatEvent.BROADCAST -> location.outgoing.broadcast ?: defaults.broadcast 53 | ChatEvent.STATUS -> location.outgoing.status ?: defaults.status 54 | } 55 | 56 | if (!matchesEvent) { 57 | logger.debug("location: $label dropped message '$msg' from user: '$user', event not enabled") 58 | logger.debug("event: $event") 59 | logger.debug("location.outgoing: ${location.outgoing}") 60 | logger.debug("defaults: $defaults") 61 | continue 62 | } 63 | 64 | skips.addAll(location.outgoing.skip) 65 | 66 | val eventStr = when (event) { 67 | ChatEvent.PLAIN -> "" 68 | ChatEvent.ACTION -> ApiMessage.USER_ACTION 69 | ChatEvent.DEATH -> "" 70 | ChatEvent.JOIN -> ApiMessage.JOIN_LEAVE 71 | ChatEvent.LEAVE -> ApiMessage.JOIN_LEAVE 72 | ChatEvent.ADVANCEMENT -> "" 73 | ChatEvent.BROADCAST -> "" 74 | ChatEvent.STATUS -> "" 75 | } 76 | 77 | val username = when { 78 | systemuser -> cfg.outgoing.systemUser 79 | else -> user 80 | } 81 | val avatar = when { 82 | systemuser -> 83 | cfg.outgoing.avatar.systemUserAvatar 84 | cfg.outgoing.avatar.enable && uuid != null -> 85 | cfg.outgoing.avatar.urlTemplate.replace("{uuid}", uuid.toString()) 86 | else -> 87 | null 88 | } 89 | when { 90 | msg.isNotBlank() -> MessageHandlerInst.transmit( 91 | ApiMessage( 92 | username = username.stripColorOut, 93 | text = msg.stripColorOut, 94 | event = eventStr, 95 | gateway = location.gateway 96 | ).apply { 97 | avatar?.let { 98 | this.avatar = it 99 | } 100 | }, 101 | cause = cause 102 | ) 103 | else -> logger.warn("WARN: dropped blank message by '$user'") 104 | } 105 | logger.debug("sent message through location: $label, cause: $cause") 106 | handled = true 107 | } 108 | return handled 109 | } 110 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/bridge/command/BridgeCommandRegistry.kt: -------------------------------------------------------------------------------- 1 | package matterlink.bridge.command 2 | 3 | import matterlink.api.ApiMessage 4 | import matterlink.config.CommandConfig 5 | import matterlink.config.IdentitiesConfig 6 | import matterlink.config.PermissionConfig 7 | import matterlink.config.cfg 8 | import matterlink.logger 9 | import java.util.HashMap 10 | import java.util.UUID 11 | 12 | object BridgeCommandRegistry { 13 | 14 | private val commandMap: HashMap = hashMapOf() 15 | 16 | /** 17 | * 18 | * @return consume message flag 19 | */ 20 | suspend fun handleCommand(input: ApiMessage): Boolean { 21 | if (!cfg.command.enable || input.text.isBlank()) return false 22 | 23 | if (input.text[0] != cfg.command.prefix || input.text.length < 2) return false 24 | 25 | val cmd = input.text.substring(1).split(' ', ignoreCase = false, limit = 2) 26 | val args = if (cmd.size == 2) cmd[1] else "" 27 | 28 | val uuid = IdentitiesConfig.getUUID(input.account, input.userid) 29 | 30 | val env = IBridgeCommand.CommandEnvironment.BridgeEnv( 31 | input.username, 32 | input.userid, 33 | input.account, 34 | input.gateway, 35 | uuid 36 | ) 37 | return commandMap[cmd[0]]?.let { 38 | if (!it.reachedTimeout()) { 39 | logger.debug("dropped command ${it.alias}") 40 | return false 41 | } 42 | it.preExecute() // resets the tickCounter 43 | if (!it.canExecute(uuid)) { 44 | env.respond( 45 | text = "${input.username} is not permitted to perform command: ${cmd[0]}" 46 | ) 47 | return false 48 | } 49 | it.execute(cmd[0], input.username, env, args) 50 | } ?: false 51 | } 52 | 53 | suspend fun handleCommand(text: String, username: String, uuid: UUID): Boolean { 54 | if (!cfg.command.enable || text.isBlank()) return false 55 | 56 | if (text[0] != cfg.command.prefix || text.length < 2) return false 57 | 58 | val cmd = text.substring(1).split(' ', ignoreCase = false, limit = 2) 59 | val args = if (cmd.size == 2) cmd[1] else "" 60 | 61 | val env = IBridgeCommand.CommandEnvironment.GameEnv(username, uuid) 62 | 63 | return commandMap[cmd[0]]?.let { 64 | if (!it.reachedTimeout()) { 65 | logger.debug("dropped command ${it.alias}") 66 | return false 67 | } 68 | it.preExecute() // resets the tickCounter 69 | if (!it.canExecute(uuid)) { 70 | env.respond( 71 | text = "$username is not permitted to perform command: ${cmd[0]}" 72 | ) 73 | return false 74 | } 75 | 76 | it.execute(cmd[0], username, env, args) 77 | } ?: false 78 | } 79 | 80 | fun register(alias: String, cmd: IBridgeCommand): Boolean { 81 | if (alias.isBlank() || commandMap.containsKey(alias)) { 82 | logger.error("Failed to register command: '$alias'") 83 | return false 84 | } 85 | if (!cmd.validate()) { 86 | logger.error("Failed to validate command: '$alias'") 87 | return false 88 | } 89 | //TODO: maybe write alias to command here ? 90 | // could avoid searching for the command in the registry 91 | commandMap[alias] = cmd 92 | return true 93 | } 94 | 95 | fun getHelpString(cmd: String): String { 96 | if (!commandMap.containsKey(cmd)) return "No such command." 97 | 98 | val help = commandMap[cmd]!!.help 99 | 100 | return if (help.isNotBlank()) help else "No help for '$cmd'" 101 | } 102 | 103 | fun getCommandList(permLvl: Double): String { 104 | return commandMap 105 | .filterValues { 106 | it.permLevel <= permLvl 107 | } 108 | .keys 109 | .joinToString(" ") 110 | } 111 | 112 | fun reloadCommands() { 113 | commandMap.clear() 114 | register("help", HelpCommand) 115 | if (cfg.command.authRequests) 116 | register("auth", AuthBridgeCommand) 117 | if (cfg.command.permisionRequests) 118 | register("request", RequestPermissionsCommand) 119 | PermissionConfig.loadFile() 120 | CommandConfig.loadFile() 121 | IdentitiesConfig.loadFile() 122 | 123 | CommandConfig.commands.forEach { (alias, command) -> 124 | register(alias, command) 125 | } 126 | } 127 | 128 | operator fun get(command: String) = commandMap[command] 129 | 130 | fun getName(command: IBridgeCommand): String? { 131 | commandMap.forEach { (alias, cmd) -> 132 | if (command == cmd) return alias 133 | } 134 | return null 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /1.7.10/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url = 'http://files.minecraftforge.net/maven' 6 | } 7 | mavenCentral() 8 | maven { 9 | url = 'https://plugins.gradle.org/m2/' 10 | } 11 | } 12 | dependencies { 13 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '1.2-SNAPSHOT' 14 | classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion 15 | } 16 | } 17 | 18 | apply plugin: 'forge' 19 | apply plugin: 'com.matthewprenger.cursegradle' 20 | 21 | version = project.mc_version + '-' + project.modVersion 22 | 23 | archivesBaseName = project.modName 24 | 25 | sourceCompatibility = targetCompatibility = '1.8' 26 | 27 | configurations { 28 | shade 29 | compile.extendsFrom shade 30 | } 31 | 32 | dependencies { 33 | shade (project(':core')) { transitive = true } 34 | shade (project(':Jankson')) { transitive = false } 35 | 36 | shade group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion 37 | 38 | shade(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion) 39 | shade(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion) 40 | 41 | shade(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion) 42 | shade(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion) 43 | shade(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion) 44 | shade(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion) 45 | 46 | // shade group: 'com.github.kittinunf.fuel', name: 'fuel', version: fuelVersion 47 | // shade group: 'com.github.kittinunf.result', name: 'result', version: resultVersion 48 | } 49 | 50 | 51 | minecraft { 52 | version = project.mc_version + '-' + project.forge_version + '-' + project.mc_version 53 | runDir = 'run' 54 | 55 | mappings = project.mcp_mappings 56 | 57 | // srgExtra 'PK: kotlin matterlink/repack/kotlin' 58 | // srgExtra 'PK: org/jetbrains/annotations matterlink/repack/org/jetbrains/annotations' 59 | // srgExtra 'PK: org/intellij matterlink/repack/org/intellij' 60 | // srgExtra 'PK: blue/endless/ matterlink/repack/blue/endless/' 61 | 62 | } 63 | 64 | compileKotlin.doFirst { 65 | def target = 'src/main/kotlin/matterlink/gen' 66 | copy { 67 | from('src/templates/kotlin/matterlink/Constants.kt') 68 | into(target) 69 | } 70 | ant.replaceregexp(match: '@MODVERSION@', replace: project.modVersion, flags: 'g', byline: true) { 71 | fileset(dir: target, includes: 'Constants.kt') 72 | } 73 | ant.replaceregexp(match: '@MCVERSION@', replace: project.mc_version, flags: 'g', byline: true) { 74 | fileset(dir: target, includes: 'Constants.kt') 75 | } 76 | ant.replaceregexp(match: '@FORGELIN-VERSION@', replace: project.forgelinVersion, flags: 'g', byline: true) { 77 | fileset(dir: target, includes: 'Constants.kt') 78 | } 79 | ant.replaceregexp(match: '@FORGE-VERSION@', replace: project.forge_version, flags: 'g', byline: true) { 80 | fileset(dir: target, includes: 'Constants.kt') 81 | } 82 | ant.replaceregexp(match: '@BUILD_NUMBER@', replace: System.env.BUILD_NUMBER ?: -1, flags: 'g', byline: true) { 83 | fileset(dir: target, includes: 'Constants.kt') 84 | } 85 | } 86 | 87 | processResources { 88 | // this will ensure that this task is redone when the versions change. 89 | inputs.property 'version', project.modVersion 90 | inputs.property 'mcversion', project.minecraft.version 91 | 92 | // replace stuff in mcmod.info, nothing else 93 | from(project(':core').sourceSets.main.resources.srcDirs) { 94 | include 'mcmod.info' 95 | 96 | // replace version and mcversion 97 | expand 'version': project.modVersion, 'mcversion': project.minecraft.version 98 | } 99 | 100 | // copy everything else except the mcmod.info 101 | from(project(':core').sourceSets.main.resources.srcDirs) { 102 | exclude 'mcmod.info' 103 | } 104 | } 105 | 106 | jar { 107 | configurations.shade.each { dep -> 108 | from(project.zipTree(dep)) { 109 | exclude 'META-INF', 'META-INF/**' 110 | } 111 | } 112 | } 113 | 114 | curseforge { 115 | if (project.hasProperty('CURSEFORGE_API_TOKEN') && project.hasProperty('release')) { 116 | apiKey = CURSEFORGE_API_TOKEN 117 | } 118 | project { 119 | id = project.curseId 120 | releaseType = project.curseReleaseType 121 | if (project.hasProperty('changelog_file')) { 122 | println("changelog = $changelog_file") 123 | changelogType = 'markdown' 124 | changelog = file(changelog_file) 125 | } 126 | mainArtifact(jar) { 127 | displayName = "MatterLink $version" 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/config/IdentitiesConfig.kt: -------------------------------------------------------------------------------- 1 | package matterlink.config 2 | 3 | import blue.endless.jankson.Jankson 4 | import blue.endless.jankson.JsonObject 5 | import blue.endless.jankson.impl.Marshaller 6 | import blue.endless.jankson.impl.SyntaxError 7 | import com.google.common.cache.Cache 8 | import com.google.common.cache.CacheBuilder 9 | import matterlink.getList 10 | import matterlink.logger 11 | import matterlink.stackTraceString 12 | import java.io.File 13 | import java.io.FileNotFoundException 14 | import java.util.UUID 15 | import java.util.concurrent.TimeUnit 16 | 17 | typealias IdentMap = Map>> 18 | 19 | data class AuthRequest( 20 | val username: String, 21 | val uuid: String, 22 | val nonce: String, 23 | val platform: String, 24 | val userid: String 25 | ) 26 | 27 | object IdentitiesConfig { 28 | val authRequests: Cache = CacheBuilder.newBuilder() 29 | .expireAfterWrite(10, TimeUnit.MINUTES) 30 | .build() 31 | 32 | private val jankson = Jankson 33 | .builder() 34 | .build() 35 | 36 | private val configFile: File = baseCfg.cfgDirectory.resolve("identities.hjson") 37 | 38 | private val default = mapOf( 39 | ("edd31c45-b095-49c5-a9f5-59cec4cfed8c" to mapOf( 40 | "discord.game" to (listOf("112228624366575616") to "discord id") 41 | ) to "username: NikkyAi") 42 | ) 43 | 44 | var idents: IdentMap = mapOf() 45 | private set 46 | 47 | private var jsonObject: JsonObject = JsonObject() 48 | 49 | fun loadFile() { 50 | 51 | val defaultJsonObject = JsonObject().apply { 52 | default.forEach { (uuid, userMap), uuidComment -> 53 | val jsonUserMap = this.putDefault(uuid, JsonObject(), uuidComment) 54 | if (jsonUserMap is JsonObject) { 55 | userMap.forEach { platform, (user, comment) -> 56 | logger.trace("loading uuid: $uuid platform: $platform user: $user") 57 | val element = Marshaller.getFallback().serialize(user) 58 | jsonUserMap.putDefault(platform, element, comment.takeUnless { it.isBlank() }) 59 | } 60 | this[uuid] = jsonUserMap 61 | } else { 62 | logger.error("cannot parse uuid: $uuid , value: '$jsonUserMap' as Map, skipping") 63 | } 64 | } 65 | } 66 | 67 | var save = true 68 | jsonObject = try { 69 | jankson.load(configFile) 70 | } catch (e: SyntaxError) { 71 | logger.error("error parsing config: ${e.completeMessage}") 72 | save = false 73 | defaultJsonObject 74 | } catch (e: FileNotFoundException) { 75 | logger.error("cannot find config: $configFile .. creating sample permissions mapping") 76 | configFile.createNewFile() 77 | defaultJsonObject 78 | } 79 | 80 | load(save) 81 | } 82 | 83 | private fun load(save: Boolean = true) { 84 | val tmpIdents: MutableMap>> = mutableMapOf() 85 | jsonObject.forEach { uuid, jsonIdentifier -> 86 | val identMap: MutableMap> = tmpIdents[uuid]?.toMutableMap() ?: mutableMapOf() 87 | if (jsonIdentifier is JsonObject) { 88 | jsonIdentifier.forEach { platform, user -> 89 | logger.info("$uuid $platform $user") 90 | identMap[platform] = jsonIdentifier.getList(platform) ?: emptyList() 91 | } 92 | } 93 | tmpIdents[uuid] = identMap.toMap() 94 | } 95 | idents = tmpIdents.toMap() 96 | 97 | logger.info("Identities loaded") 98 | 99 | if (save) 100 | configFile.writeText(jsonObject.toJson(true, true)) 101 | } 102 | 103 | fun add(uuid: String, username: String, platform: String, userid: String, comment: String? = null) { 104 | val platformObject = jsonObject.getObject(uuid) ?: JsonObject() 105 | platformObject.putDefault(platform, userid, comment) 106 | val userIdList = platformObject.getList(platform) ?: emptyList() 107 | platformObject[platform] = platformObject.marshaller.serialize(userIdList + userid) 108 | jsonObject[uuid] = platformObject 109 | 110 | if (jsonObject.getComment(uuid) == null) { 111 | jsonObject.setComment(uuid, "Username: $username") 112 | } 113 | 114 | load() 115 | } 116 | 117 | //TODO: rewrite, store ident map differently in memory 118 | fun getUUID(platform: String, userid: String): UUID? { 119 | return idents.entries.firstOrNull { (uuid, usermap) -> 120 | usermap.entries.any { (_platform, userids) -> 121 | if (platform.equals(_platform, true)) 122 | logger.info("platform: $_platform userids: $userids") 123 | platform.equals(_platform, true) && userids.contains(userid) 124 | } 125 | }?.key?.let { 126 | try { 127 | UUID.fromString(it) 128 | } catch (e: IllegalArgumentException) { 129 | logger.error("cannot parse UUID: $it") 130 | logger.error(e.stackTraceString) 131 | null 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/Util.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import blue.endless.jankson.Jankson 4 | import blue.endless.jankson.JsonArray 5 | import blue.endless.jankson.JsonElement 6 | import blue.endless.jankson.JsonObject 7 | import blue.endless.jankson.impl.Marshaller 8 | import matterlink.config.cfg 9 | import java.io.PrintWriter 10 | import java.io.StringWriter 11 | import java.util.* 12 | 13 | private const val ZWSP: Char = '\u200b' 14 | 15 | //Inserts a zero-width space at index 1 in the string' 16 | 17 | val String.antiping: String 18 | get() { 19 | return this[0].toString() + ZWSP + this.substring(1) 20 | } 21 | 22 | fun String.mapFormat(env: Map): String { 23 | var result = this 24 | env.forEach { key, value -> 25 | if (result.contains(key)) { 26 | result = result.replace(key, value) 27 | } 28 | } 29 | return result 30 | } 31 | 32 | fun String.lazyFormat(env: Map String?>): String { 33 | var result = this 34 | env.forEach { key, value -> 35 | if (result.contains(key)) { 36 | result = result.replace(key, value().toString()) 37 | } 38 | } 39 | return result 40 | } 41 | 42 | val String.stripColorOut: String 43 | get() = 44 | if (cfg.outgoing.stripColors) 45 | this.replace("[&§][0-9A-FK-OR]".toRegex(RegexOption.IGNORE_CASE), "") 46 | else 47 | this 48 | 49 | 50 | val String.stripColorIn: String 51 | get() = if (cfg.incoming.stripColors) 52 | this.replace("§.?".toRegex(), "") 53 | else 54 | this 55 | 56 | 57 | val Exception.stackTraceString: String 58 | get() { 59 | val sw = StringWriter() 60 | this.printStackTrace(PrintWriter(sw)) 61 | return sw.toString() 62 | } 63 | 64 | fun randomString(length: Int = 6): String = 65 | java.util.UUID.randomUUID().toString().replace("-", "").take(length) 66 | 67 | fun JsonObject.getOrDefault(key: String, default: T, comment: String? = null): T { 68 | logger.trace("type: ${default.javaClass.name} key: $key json: >>>${this.getObject(key)?.toJson()}<<< default: $default") 69 | return putDefault(key, default, comment)!!.also { 70 | setComment(key, comment) 71 | } 72 | } 73 | 74 | inline fun Jankson.fromJson(obj: JsonObject): T = this.fromJson(obj, T::class.java) 75 | inline fun Jankson.fromJson(json: String): T = this.fromJson(json, T::class.java) 76 | 77 | inline fun Jankson.Builder.registerTypeAdapter(noinline adapter: (JsonObject) -> T) = 78 | this.registerTypeAdapter(T::class.java, adapter) 79 | 80 | inline fun Jankson.Builder.registerPrimitiveTypeAdapter(noinline adapter: (Any) -> T) = 81 | this.registerPrimitiveTypeAdapter(T::class.java, adapter) 82 | 83 | inline fun Jankson.Builder.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) = 84 | this.registerSerializer(T::class.java, serializer) 85 | 86 | inline fun Marshaller.registerSerializer(noinline serializer: (T) -> JsonElement) = 87 | this.registerSerializer(T::class.java, serializer) 88 | 89 | inline fun Marshaller.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) = 90 | this.registerSerializer(T::class.java, serializer) 91 | 92 | inline fun JsonObject.getReified(key: String, comment: String? = null): T? = 93 | this.get(T::class.java, key) 94 | ?.also { setComment(key, comment) } 95 | 96 | inline fun JsonObject.getReifiedOrDelete(key: String, comment: String? = null): T? = 97 | this.get(T::class.java, key) 98 | ?.also { setComment(key, comment) } 99 | ?: run { 100 | this.remove(key) 101 | null 102 | } 103 | 104 | inline fun JsonObject.getList(key: String): List? { 105 | return this[key]?.let { array -> 106 | when (array) { 107 | is JsonArray -> { 108 | array.indices.map { i -> 109 | array.get(T::class.java, i) ?: throw NullPointerException("cannot parse ${array.get(i)}") 110 | } 111 | } 112 | else -> null 113 | } 114 | } 115 | } 116 | 117 | inline fun JsonObject.getOrPutList(key: String, default: List, comment: String?): List { 118 | return this[key]?.let { array -> 119 | when (array) { 120 | is JsonArray -> { 121 | array.indices.map { i -> 122 | array.get(T::class.java, i) ?: throw NullPointerException("cannot parse ${array.get(i)}") 123 | } 124 | } 125 | else -> null 126 | } 127 | }.also { 128 | setComment(key, comment) 129 | } ?: this.putDefault(key, default, comment) ?: default 130 | } 131 | 132 | inline fun JsonObject.getOrPutMap( 133 | key: String, 134 | default: Map, 135 | comment: String? 136 | ): Map { 137 | return this[key]?.let { map -> 138 | when (map) { 139 | is JsonObject -> { 140 | map.mapValues { (key, element) -> 141 | map.get(T::class.java, key) ?: throw NullPointerException("cannot parse $element") 142 | } 143 | } 144 | else -> null 145 | } 146 | }.also { 147 | setComment(key, comment) 148 | } ?: this.putDefault(key, default, comment) ?: default 149 | } -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import matterlink.config.cfg 5 | import matterlink.handlers.ChatEvent 6 | import matterlink.handlers.ChatProcessor 7 | import matterlink.handlers.DeathHandler 8 | import matterlink.handlers.JoinLeaveHandler 9 | import matterlink.handlers.ProgressHandler 10 | import matterlink.handlers.TickHandler 11 | import net.minecraft.command.server.CommandBroadcast 12 | import net.minecraft.command.server.CommandEmote 13 | import net.minecraft.entity.player.EntityPlayer 14 | import net.minecraft.server.dedicated.DedicatedServer 15 | import net.minecraftforge.event.CommandEvent 16 | import net.minecraftforge.event.ServerChatEvent 17 | import net.minecraftforge.event.entity.living.LivingDeathEvent 18 | import net.minecraftforge.event.entity.player.AdvancementEvent 19 | import net.minecraftforge.fml.common.Mod 20 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent 21 | import net.minecraftforge.fml.common.gameevent.PlayerEvent 22 | import net.minecraftforge.fml.common.gameevent.TickEvent 23 | 24 | //FORGE-DEPENDENT 25 | @Mod.EventBusSubscriber 26 | object EventHandler { 27 | 28 | //MC-VERSION & FORGE DEPENDENT 29 | @SubscribeEvent 30 | @JvmStatic 31 | fun progressEvent(e: AdvancementEvent) = runBlocking { 32 | if (e.advancement.display == null) return@runBlocking 33 | ProgressHandler.handleProgress( 34 | name = e.entityPlayer.displayName.unformattedText, 35 | message = "has made the advancement", 36 | display = e.advancement.displayText.unformattedText, 37 | x = e.entityPlayer.posX.toInt(), 38 | y = e.entityPlayer.posY.toInt(), 39 | z = e.entityPlayer.posZ.toInt(), 40 | dimension = e.entityPlayer.dimension 41 | ) 42 | } 43 | 44 | //FORGE-DEPENDENT 45 | @SubscribeEvent 46 | @JvmStatic 47 | fun chatEvent(e: ServerChatEvent) = runBlocking { 48 | if (e.isCanceled) return@runBlocking 49 | e.isCanceled = ChatProcessor.sendToBridge( 50 | user = e.player.displayName.unformattedText, 51 | msg = e.message, 52 | x = e.player.posX.toInt(), 53 | y = e.player.posY.toInt(), 54 | z = e.player.posZ.toInt(), 55 | dimension = e.player.dimension, 56 | event = ChatEvent.PLAIN, 57 | uuid = e.player.gameProfile.id 58 | ) 59 | } 60 | 61 | //FORGE-DEPENDENT 62 | @SubscribeEvent 63 | @JvmStatic 64 | fun commandEvent(e: CommandEvent) = runBlocking { 65 | 66 | val sender = when { 67 | e.sender is DedicatedServer -> cfg.outgoing.systemUser 68 | else -> e.sender.displayName.unformattedText 69 | } 70 | val args = e.parameters.joinToString(" ") 71 | val type = with(e.command) { 72 | when { 73 | this is CommandEmote || name.equals("me", true) -> ChatEvent.ACTION 74 | this is CommandBroadcast || name.equals("say", true) -> ChatEvent.BROADCAST 75 | else -> return@runBlocking 76 | } 77 | } 78 | ChatProcessor.sendToBridge( 79 | user = sender, 80 | msg = args, 81 | event = type, 82 | x = e.sender.position.x, 83 | y = e.sender.position.y, 84 | z = e.sender.position.z, 85 | dimension = when { 86 | e.sender is DedicatedServer -> null 87 | else -> e.sender.commandSenderEntity?.dimension ?: e.sender.entityWorld.provider.dimension 88 | } 89 | ) 90 | } 91 | 92 | //FORGE-DEPENDENT 93 | @SubscribeEvent 94 | @JvmStatic 95 | fun deathEvent(e: LivingDeathEvent) = runBlocking { 96 | if (e.entityLiving is EntityPlayer) { 97 | DeathHandler.handleDeath( 98 | player = e.entityLiving.displayName.unformattedText, 99 | deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText, 100 | damageType = e.source.damageType, 101 | x = e.entityLiving.posX.toInt(), 102 | y = e.entityLiving.posY.toInt(), 103 | z = e.entityLiving.posZ.toInt(), 104 | dimension = e.entityLiving.dimension 105 | ) 106 | } 107 | } 108 | 109 | //FORGE-DEPENDENT 110 | @SubscribeEvent 111 | @JvmStatic 112 | fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking { 113 | JoinLeaveHandler.handleJoin( 114 | player = e.player.displayName.unformattedText, 115 | x = e.player.posX.toInt(), 116 | y = e.player.posY.toInt(), 117 | z = e.player.posZ.toInt(), 118 | dimension = e.player.dimension 119 | ) 120 | } 121 | 122 | //FORGE-DEPENDENT 123 | @SubscribeEvent 124 | @JvmStatic 125 | fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking { 126 | JoinLeaveHandler.handleLeave( 127 | player = e.player.displayName.unformattedText, 128 | x = e.player.posX.toInt(), 129 | y = e.player.posY.toInt(), 130 | z = e.player.posZ.toInt(), 131 | dimension = e.player.dimension 132 | ) 133 | } 134 | 135 | //FORGE-DEPENDENT 136 | @SubscribeEvent 137 | @JvmStatic 138 | fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking { 139 | if (e.phase == TickEvent.Phase.END) 140 | TickHandler.handleTick() 141 | 142 | } 143 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/handlers/ServerChatHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.handlers 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import matterlink.api.ApiMessage 5 | import matterlink.bridge.MessageHandlerInst 6 | import matterlink.bridge.command.BridgeCommandRegistry 7 | import matterlink.bridge.format 8 | import matterlink.config.cfg 9 | import matterlink.instance 10 | import matterlink.logger 11 | import java.util.UUID 12 | 13 | object ServerChatHandler { 14 | @UseExperimental(ExperimentalCoroutinesApi::class) 15 | val rcvChannel = MessageHandlerInst.broadcast.openSubscription() 16 | 17 | /** 18 | * This method must be called every server tick with no arguments. 19 | */ 20 | suspend fun writeIncomingToChat() { 21 | val nextMessage = rcvChannel.poll() ?: return 22 | 23 | val defaults = cfg.incomingDefaults 24 | 25 | val sourceGateway = nextMessage.gateway 26 | 27 | // find all areas to send to 28 | 29 | val targetUUIDs = mutableSetOf() 30 | val skips = mutableSetOf() 31 | 32 | val locations = cfg.locations.filter { 33 | it.gateway == sourceGateway 34 | } 35 | 36 | if (nextMessage.event.isEmpty()) { 37 | // filter command handlers 38 | val commandLocations = locations.filter { 39 | it.incoming.commands ?: cfg.incomingDefaults.commands 40 | } 41 | 42 | // process potential command 43 | for ((label, location) in commandLocations) { 44 | if (BridgeCommandRegistry.handleCommand(nextMessage)) return 45 | } 46 | } 47 | 48 | 49 | 50 | for (location in locations) { 51 | val label = location.label 52 | if (skips.contains(label)) { 53 | logger.debug("skipping $label") 54 | continue 55 | } 56 | val matchesEvent = when (nextMessage.event) { 57 | "" -> { 58 | // if (location.incoming.commands ?: defaults.commands 59 | // && BridgeCommandRegistry.handleCommand(nextMessage)) return 60 | location.incoming.plain ?: defaults.plain 61 | } 62 | ApiMessage.JOIN_LEAVE -> location.incoming.join_leave ?: defaults.join_leave 63 | ApiMessage.USER_ACTION -> location.incoming.action ?: defaults.action 64 | else -> { 65 | logger.error("unknown event type '${nextMessage.event}' on incoming message") 66 | return 67 | } 68 | } 69 | 70 | if (!matchesEvent) { 71 | logger.info("location: $label dropped message '$nextMessage' event not enabled") 72 | continue 73 | } 74 | 75 | targetUUIDs.addAll(instance.collectPlayers(location.area)) 76 | } 77 | 78 | val message = when (nextMessage.event) { 79 | "user_action" -> nextMessage.format(cfg.incoming.action) 80 | "" -> { 81 | // try to handle command and do not handle as a chat message 82 | if (BridgeCommandRegistry.handleCommand(nextMessage)) return 83 | nextMessage.format(cfg.incoming.chat) 84 | } 85 | "join_leave" -> nextMessage.format(cfg.incoming.joinPart) 86 | else -> { 87 | val user = nextMessage.username 88 | val text = nextMessage.text 89 | val json = nextMessage.encode() 90 | logger.debug("Threw out message with unhandled event: ${nextMessage.event}") 91 | logger.debug(" Message contents:") 92 | logger.debug(" User: $user") 93 | logger.debug(" Text: $text") 94 | logger.debug(" JSON: $json") 95 | return 96 | } 97 | } 98 | 99 | targetUUIDs.forEach { 100 | //TODO: optimize send to all at once 101 | instance.wrappedSendToPlayer(it, message) 102 | } 103 | 104 | 105 | // if (nextMessage?.gateway == cfg.connect.gateway) { 106 | // if (!nextMessage.text.isBlank()) { 107 | // nextMessage.text = nextMessage.text.stripColorIn 108 | // val message = when (nextMessage.event) { 109 | // "user_action" -> nextMessage.format(cfg.incoming.action) 110 | // "" -> { 111 | // // try to handle command and do not handle as a chat message 112 | // if (BridgeCommandRegistry.handleCommand(nextMessage)) return 113 | // nextMessage.format(cfg.incoming.chat) 114 | // } 115 | // "join_leave" -> nextMessage.format(cfg.incoming.joinPart) 116 | // else -> { 117 | // val user = nextMessage.username 118 | // val text = nextMessage.text 119 | // val json = nextMessage.encode() 120 | // logger.debug("Threw out message with unhandled event: ${nextMessage.event}") 121 | // logger.debug(" Message contents:") 122 | // logger.debug(" User: $user") 123 | // logger.debug(" Text: $text") 124 | // logger.debug(" JSON: $json") 125 | // return 126 | // } 127 | // } 128 | // instance.wrappedSendToPlayers(message) 129 | // } 130 | // } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import matterlink.config.cfg 5 | import matterlink.handlers.ChatEvent 6 | import matterlink.handlers.ChatProcessor 7 | import matterlink.handlers.DeathHandler 8 | import matterlink.handlers.JoinLeaveHandler 9 | import matterlink.handlers.ProgressHandler 10 | import matterlink.handlers.TickHandler 11 | import net.minecraft.command.server.CommandBroadcast 12 | import net.minecraft.command.server.CommandEmote 13 | import net.minecraft.entity.player.EntityPlayer 14 | import net.minecraft.entity.player.EntityPlayerMP 15 | import net.minecraft.server.dedicated.DedicatedServer 16 | import net.minecraftforge.event.CommandEvent 17 | import net.minecraftforge.event.ServerChatEvent 18 | import net.minecraftforge.event.entity.living.LivingDeathEvent 19 | import net.minecraftforge.event.entity.player.AchievementEvent 20 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent 21 | import net.minecraftforge.fml.common.gameevent.PlayerEvent 22 | import net.minecraftforge.fml.common.gameevent.TickEvent 23 | 24 | //FORGE-DEPENDENT 25 | object EventHandler { 26 | 27 | //MC-VERSION & FORGE DEPENDENT 28 | @SubscribeEvent 29 | fun progressEvent(e: AchievementEvent) = runBlocking { 30 | val achievement = e.achievement 31 | val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return@runBlocking 32 | val statFile = entityPlayer.statFile 33 | 34 | if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) { 35 | return@runBlocking 36 | } 37 | ProgressHandler.handleProgress( 38 | name = e.entityPlayer.displayName.unformattedText, 39 | message = "has earned the achievement", 40 | display = e.achievement.statName.unformattedText, 41 | x = e.entityPlayer.posX.toInt(), 42 | y = e.entityPlayer.posY.toInt(), 43 | z = e.entityPlayer.posZ.toInt(), 44 | dimension = e.entityPlayer.dimension 45 | ) 46 | } 47 | 48 | //FORGE-DEPENDENT 49 | @SubscribeEvent 50 | fun chatEvent(e: ServerChatEvent) = runBlocking { 51 | if (e.isCanceled) return@runBlocking 52 | e.isCanceled = ChatProcessor.sendToBridge( 53 | user = e.player.displayName.unformattedText, 54 | msg = e.message, 55 | x = e.player.posX.toInt(), 56 | y = e.player.posY.toInt(), 57 | z = e.player.posZ.toInt(), 58 | dimension = e.player.dimension, 59 | event = ChatEvent.PLAIN, 60 | uuid = e.player.gameProfile.id 61 | ) 62 | } 63 | 64 | //FORGE-DEPENDENT 65 | @SubscribeEvent 66 | fun commandEvent(e: CommandEvent) = runBlocking { 67 | val sender = when { 68 | e.sender is DedicatedServer -> cfg.outgoing.systemUser 69 | else -> e.sender.displayName.unformattedText 70 | } 71 | val args = e.parameters.joinToString(" ") 72 | val type = with(e.command) { 73 | when { 74 | this is CommandEmote || commandName.equals("me", true) -> ChatEvent.ACTION 75 | this is CommandBroadcast || commandName.equals("say", true) -> ChatEvent.BROADCAST 76 | else -> return@runBlocking 77 | } 78 | } 79 | ChatProcessor.sendToBridge( 80 | user = sender, 81 | msg = args, 82 | event = type, 83 | x = e.sender.position.x, 84 | y = e.sender.position.y, 85 | z = e.sender.position.z, 86 | dimension = when { 87 | e.sender is DedicatedServer -> null 88 | else -> e.sender.commandSenderEntity?.dimension ?: e.sender.entityWorld.provider.dimension 89 | } 90 | ) 91 | } 92 | 93 | //FORGE-DEPENDENT 94 | @SubscribeEvent 95 | fun deathEvent(e: LivingDeathEvent) = runBlocking { 96 | if (e.entityLiving is EntityPlayer) { 97 | DeathHandler.handleDeath( 98 | player = e.entityLiving.displayName.unformattedText, 99 | deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText, 100 | damageType = e.source.damageType, 101 | x = e.entityLiving.posX.toInt(), 102 | y = e.entityLiving.posY.toInt(), 103 | z = e.entityLiving.posZ.toInt(), 104 | dimension = e.entityLiving.dimension 105 | ) 106 | } 107 | } 108 | 109 | //FORGE-DEPENDENT 110 | @SubscribeEvent 111 | fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking { 112 | JoinLeaveHandler.handleJoin( 113 | player = e.player.displayName.unformattedText, 114 | x = e.player.posX.toInt(), 115 | y = e.player.posY.toInt(), 116 | z = e.player.posZ.toInt(), 117 | dimension = e.player.dimension 118 | ) 119 | } 120 | 121 | //FORGE-DEPENDENT 122 | @SubscribeEvent 123 | fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking { 124 | JoinLeaveHandler.handleLeave( 125 | player = e.player.displayName.unformattedText, 126 | x = e.player.posX.toInt(), 127 | y = e.player.posY.toInt(), 128 | z = e.player.posZ.toInt(), 129 | dimension = e.player.dimension 130 | ) 131 | } 132 | 133 | //FORGE-DEPENDENT 134 | @SubscribeEvent 135 | fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking { 136 | if (e.phase == TickEvent.Phase.END) 137 | TickHandler.handleTick() 138 | } 139 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /1.7.10/src/main/kotlin/matterlink/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import cpw.mods.fml.common.eventhandler.SubscribeEvent 4 | import cpw.mods.fml.common.gameevent.PlayerEvent 5 | import cpw.mods.fml.common.gameevent.TickEvent 6 | import kotlinx.coroutines.runBlocking 7 | import matterlink.config.cfg 8 | import matterlink.handlers.ChatEvent 9 | import matterlink.handlers.ChatProcessor 10 | import matterlink.handlers.DeathHandler 11 | import matterlink.handlers.JoinLeaveHandler 12 | import matterlink.handlers.ProgressHandler 13 | import matterlink.handlers.TickHandler 14 | import net.minecraft.command.server.CommandBroadcast 15 | import net.minecraft.command.server.CommandEmote 16 | import net.minecraft.entity.Entity 17 | import net.minecraft.entity.player.EntityPlayer 18 | import net.minecraft.entity.player.EntityPlayerMP 19 | import net.minecraft.server.dedicated.DedicatedServer 20 | import net.minecraftforge.event.CommandEvent 21 | import net.minecraftforge.event.ServerChatEvent 22 | import net.minecraftforge.event.entity.living.LivingDeathEvent 23 | import net.minecraftforge.event.entity.player.AchievementEvent 24 | 25 | //FORGE-DEPENDENT 26 | object EventHandler { 27 | 28 | //MC-VERSION & FORGE DEPENDENT 29 | @SubscribeEvent 30 | fun progressEvent(e: AchievementEvent) = runBlocking { 31 | val achievement = e.achievement 32 | val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return@runBlocking 33 | val statFile = entityPlayer.statFile 34 | 35 | if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) { 36 | return@runBlocking 37 | } 38 | 39 | ProgressHandler.handleProgress( 40 | name = e.entityPlayer.displayName, 41 | message = "has earned the achievement", 42 | display = e.achievement.statName.unformattedText, 43 | x = e.entityPlayer.posX.toInt(), 44 | y = e.entityPlayer.posY.toInt(), 45 | z = e.entityPlayer.posZ.toInt(), 46 | dimension = e.entityPlayer.dimension 47 | ) 48 | } 49 | 50 | //FORGE-DEPENDENT 51 | @SubscribeEvent 52 | fun chatEvent(e: ServerChatEvent) = runBlocking { 53 | if (e.isCanceled) return@runBlocking 54 | e.isCanceled = ChatProcessor.sendToBridge( 55 | user = e.player.displayName, 56 | msg = e.message, 57 | x = e.player.posX.toInt(), 58 | y = e.player.posY.toInt(), 59 | z = e.player.posZ.toInt(), 60 | dimension = e.player.dimension, 61 | event = ChatEvent.PLAIN, 62 | uuid = e.player.gameProfile.id 63 | ) 64 | } 65 | 66 | //FORGE-DEPENDENT 67 | @SubscribeEvent 68 | fun commandEvent(e: CommandEvent) = runBlocking { 69 | val sender = when { 70 | e.sender is DedicatedServer -> cfg.outgoing.systemUser 71 | else -> e.sender.commandSenderName 72 | } 73 | val args = e.parameters.joinToString(" ") 74 | val type = with(e.command) { 75 | when { 76 | this is CommandEmote || commandName.equals("me", true) -> ChatEvent.ACTION 77 | this is CommandBroadcast || commandName.equals("say", true) -> ChatEvent.BROADCAST 78 | else -> return@runBlocking 79 | } 80 | } 81 | val s = e.sender 82 | val (x, y, z) = when (s) { 83 | is Entity -> Triple(s.posX.toInt(), s.posY.toInt(), s.posZ.toInt()) 84 | else -> with(s.commandSenderPosition) { Triple(posX, posY, posZ) } 85 | } 86 | ChatProcessor.sendToBridge( 87 | user = sender, 88 | msg = args, 89 | event = type, 90 | x = x, 91 | y = y, 92 | z = z, 93 | dimension = when { 94 | e.sender is DedicatedServer -> null 95 | else -> e.sender.entityWorld.provider.dimensionId 96 | } 97 | ) 98 | 99 | } 100 | 101 | //FORGE-DEPENDENT 102 | @SubscribeEvent 103 | fun deathEvent(e: LivingDeathEvent) = runBlocking { 104 | if (e.entityLiving is EntityPlayer) { 105 | val player = e.entityLiving as EntityPlayer 106 | DeathHandler.handleDeath( 107 | player = player.displayName, 108 | deathMessage = e.entityLiving.combatTracker.func_151521_b().unformattedText, 109 | damageType = e.source.damageType, 110 | x = e.entityLiving.posX.toInt(), 111 | y = e.entityLiving.posY.toInt(), 112 | z = e.entityLiving.posZ.toInt(), 113 | dimension = e.entityLiving.dimension 114 | ) 115 | } 116 | } 117 | 118 | //FORGE-DEPENDENT 119 | @SubscribeEvent 120 | fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking { 121 | JoinLeaveHandler.handleJoin( 122 | player = e.player.displayName, 123 | x = e.player.posX.toInt(), 124 | y = e.player.posY.toInt(), 125 | z = e.player.posZ.toInt(), 126 | dimension = e.player.dimension 127 | ) 128 | } 129 | 130 | //FORGE-DEPENDENT 131 | @SubscribeEvent 132 | fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking { 133 | JoinLeaveHandler.handleLeave( 134 | player = e.player.displayName, 135 | x = e.player.posX.toInt(), 136 | y = e.player.posY.toInt(), 137 | z = e.player.posZ.toInt(), 138 | dimension = e.player.dimension 139 | ) 140 | } 141 | 142 | //FORGE-DEPENDENT 143 | @SubscribeEvent 144 | fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking { 145 | if (e.phase == TickEvent.Phase.END) 146 | TickHandler.handleTick() 147 | } 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/discord/176780432371744769.svg?style=for-the-badge&label=%23ai-dev&logo=discord)](http://discord.gg/Fm5EST) 2 | [![Discord](https://img.shields.io/discord/342696338556977153.svg?style=for-the-badge&logo=discord)](https://discord.gg/hXqNgq5) 3 | [![Download 1.12.2](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.12.2)](https://curse.nikky.moe/api/url/287323?version=1.12.2) 4 | [![Jenkins](https://img.shields.io/jenkins/s/https/ci.elytradev.com/job/elytra/job/MatterLink/job/master.svg?style=for-the-badge&label=Jenkins%20Build)](https://ci.elytradev.com/job/elytra/job/MatterLink/job/master/lastSuccessfulBuild/artifact/) 5 | [![Patreon](https://img.shields.io/badge/Patreon-Nikkyai-red.svg?style=for-the-badge)](https://www.patreon.com/NikkyAi) 6 | 7 | # MatterLink 8 | 9 | - [Downloads](#downloads) 10 | - [Dependencies](#dependencies) 11 | - [Features](#features) 12 | - [Setup](#setup) 13 | 14 | A Matterbridge endpoint for MC servers! 15 | 16 | THIS MOD REQUIRES YOU TO ALSO RUN A MATTERBRIDGE RELAY 17 | https://github.com/42wim/matterbridge 18 | 19 | Chat with us on IRC: [#matterlink @ irc.esper.net](irc://irc.esper.net/matterlink) 20 | 21 | ## Downloads 22 | 23 | [![Github All Releases](https://img.shields.io/github/downloads/elytra/MatterLink/total.svg?style=for-the-badge&label=Github%20Releases&logo=github)](https://github.com/elytra/MatterLink/releases) 24 | 25 | [![Jenkins](https://img.shields.io/jenkins/s/https/ci.elytradev.com/job/elytra/job/MatterLink/job/master.svg?style=for-the-badge&label=Jenkins%20Build)](https://ci.elytradev.com/job/elytra/job/MatterLink/job/master/lastSuccessfulBuild/artifact/) 26 | 27 | [![Files](https://curse.nikky.moe/api/img/287323/files?logo&style=for-the-badge&version=1.12.2)](https://minecraft.curseforge.com/projects/287323/files) 28 | 29 | [![Download 1.12.2](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.12.2)](https://curse.nikky.moe/api/url/287323?version=1.12.2) 30 | 31 | [![Download 1.9.4](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.9.4)](https://curse.nikky.moe/api/url/287323?version=1.9.4) 32 | 33 | ## Dependencies 34 | 35 | [![Forgelin Files](https://curse.nikky.moe/api/img/248453/files?logo&style=for-the-badge)](https://minecraft.curseforge.com/projects/248453/files) 36 | 37 | ## Features 38 | 39 | ### Custom bridge commands 40 | 41 | includes pass-through to Minecraft commands! 42 | Default commands: `help, tps, list, seed, uptime` 43 | 44 | Commands are specified in JSON format as follows: 45 | 46 | Passthrough command (executes the configured command from the MC server console) 47 | 48 | ```json 49 | { 50 | "tps": { 51 | "type": "PASSTHROUGH", 52 | "execute": "forge tps", 53 | "permLevel": 0, 54 | "help": "Print server tps", 55 | "allowArgs": false 56 | } 57 | } 58 | ``` 59 | 60 | Response command 61 | 62 | ```json 63 | { 64 | "uptime": { 65 | "type": "RESPONSE", 66 | "response": "{uptime}", 67 | "permLevel": 1, 68 | "help": "Print server uptime", 69 | "allowArgs": false 70 | } 71 | } 72 | ``` 73 | 74 | ### Acount Linking 75 | 76 | To link your chat account to your minecraft uuid 77 | execute `!auth Username` 78 | make sure to use the proper username and command prefix, the system will then guide you through 79 | 80 | internally the identity links are stored like so: 81 | 82 | ```json 83 | { 84 | /* username: NikkyAi */ 85 | "edd31c45-b095-49c5-a9f5-59cec4cfed8c": { 86 | /* discord id */ 87 | "discord.game": [ 88 | "112228624366575616" 89 | ] 90 | } 91 | } 92 | ``` 93 | 94 | ### Command permissions 95 | 96 | Higher numbers mean more permissions. Configured per uuid. 97 | 98 | ```json 99 | { 100 | "edd31c45-b095-49c5-a9f5-59cec4cfed8c": 9000 101 | } 102 | ``` 103 | 104 | ### Reload 105 | 106 | Edit and reload the config file without restarting the server! 107 | ``` 108 | /ml 109 | connect: Connects the MC chat to the MatterBridge server 110 | disconnect: Disconnects the chat from the MatterBridge server 111 | reload: Disconnects, reloads the config and custom command files, 112 | then reconnects. 113 | ``` 114 | 115 | ## Setup 116 | 117 | Requires the matterbridge config api section to be setup along these lines: 118 | 119 | ### Local 120 | 121 | If ou know the matterbridge will run on the same machine as the Minecraft Server 122 | ``` 123 | [api] 124 | [api.local] 125 | BindAddress="127.0.0.1:4242" // Listens only for localhost 126 | #OPTIONAL (no authorization if token is empty) 127 | Token="" # Token left empty 128 | Buffer=1000 129 | RemoteNickFormat="{NICK}" 130 | ShowJoinPart = true 131 | ``` 132 | 133 | With this you need no extra configuration steps.. just run matterbridge and then start the minecraft server (or reload matterlink with command if it runs already) 134 | 135 | ### Remote 136 | 137 | If the matterbridge runs on a different machine 138 | 139 | ``` 140 | [api] 141 | [api.local] 142 | BindAddress="0.0.0.0:4242" 143 | #OPTIONAL (no authorization if token is empty) 144 | Token="mytoken" 145 | Buffer=1000 146 | RemoteNickFormat="{NICK}" 147 | ShowJoinPart = true 148 | ``` 149 | 150 | you need to know the ip / domain of the matterbridge and the token used, 151 | enter them in the ´connection' section in the config and reload matterlink 152 | 153 | 154 | ### Sample 155 | 156 | Install matterbridge and try out the basic sample: 157 | 158 | ``` 159 | go get github.com/42wim/matterbridge 160 | mv matterbridge-sample.toml matterbridge.toml 161 | matterbridge 162 | ``` 163 | 164 | now start the server with matterlink (and forgelin) in the mods folder 165 | 166 | and then [RTFM!!!](https://github.com/42wim/matterbridge#configuration) and configure all your needed gateways, endpoints etc 167 | 168 | powered by wishful thinking -------------------------------------------------------------------------------- /1.7.10/src/main/kotlin/matterlink/MatterLink.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import com.mojang.authlib.GameProfile 4 | import cpw.mods.fml.common.FMLCommonHandler 5 | import cpw.mods.fml.common.Mod 6 | import cpw.mods.fml.common.event.FMLInitializationEvent 7 | import cpw.mods.fml.common.event.FMLPreInitializationEvent 8 | import cpw.mods.fml.common.event.FMLServerStartingEvent 9 | import cpw.mods.fml.common.event.FMLServerStoppingEvent 10 | import kotlinx.coroutines.runBlocking 11 | import matterlink.bridge.command.IBridgeCommand 12 | import matterlink.command.AuthCommand 13 | import matterlink.command.MatterLinkCommand 14 | import matterlink.command.MatterLinkCommandSender 15 | import matterlink.config.BaseConfig 16 | import matterlink.config.cfg 17 | import net.minecraft.entity.player.EntityPlayerMP 18 | import net.minecraft.server.MinecraftServer 19 | import net.minecraft.util.ChatComponentText 20 | import net.minecraftforge.common.ForgeVersion 21 | import net.minecraftforge.common.MinecraftForge 22 | import java.util.UUID 23 | 24 | @Mod( 25 | modid = MODID, 26 | name = NAME, version = MODVERSION, 27 | useMetadata = true, 28 | acceptableRemoteVersions = "*" 29 | ) 30 | class MatterLink : IMatterLink() { 31 | init { 32 | instance = this 33 | } 34 | 35 | @Mod.EventHandler 36 | fun preInit(event: FMLPreInitializationEvent) { 37 | MinecraftForge.EVENT_BUS.register(EventHandler) 38 | FMLCommonHandler.instance().bus().register(EventHandler) 39 | 40 | logger = with(event.modLog) { 41 | object : Logger { 42 | override fun info(message: String) = this@with.info(message) 43 | override fun debug(message: String) = this@with.debug(message) 44 | override fun error(message: String) = this@with.error(message) 45 | override fun warn(message: String) = this@with.warn(message) 46 | override fun trace(message: String) = this@with.trace(message) 47 | } 48 | } 49 | 50 | logger.info("Building bridge!") 51 | 52 | cfg = BaseConfig(event.modConfigurationDirectory).load() 53 | } 54 | 55 | @Mod.EventHandler 56 | fun init(event: FMLInitializationEvent) { 57 | this.registerBridgeCommands() 58 | } 59 | 60 | @Mod.EventHandler 61 | fun serverStarting(event: FMLServerStartingEvent) = runBlocking { 62 | logger.debug("Registering server commands") 63 | event.registerServerCommand(MatterLinkCommand) 64 | event.registerServerCommand(AuthCommand) 65 | start() 66 | } 67 | 68 | @Mod.EventHandler 69 | fun serverStopping(event: FMLServerStoppingEvent) = runBlocking { 70 | stop() 71 | } 72 | 73 | //FORGE-DEPENDENT 74 | override fun wrappedSendToPlayers(msg: String) { 75 | MinecraftServer.getServer().configurationManager.sendChatMsg(ChatComponentText(msg)) 76 | } 77 | 78 | override fun wrappedSendToPlayer(username: String, msg: String) { 79 | val profile = profileByName(username) ?: run { 80 | logger.error("cannot find player by name $username") 81 | return 82 | } 83 | val player = playerByProfile(profile) ?: run { 84 | logger.error("${profile.name} is not online") 85 | return 86 | } 87 | player.addChatMessage(ChatComponentText(msg)) 88 | } 89 | 90 | override fun wrappedSendToPlayer(uuid: UUID, msg: String) { 91 | val profile = profileByUUID(uuid) ?: run { 92 | logger.error("cannot find player by uuid $uuid") 93 | return 94 | } 95 | val player = playerByProfile(profile) ?: run { 96 | logger.error("${profile.name} is not online") 97 | return 98 | } 99 | player.addChatMessage(ChatComponentText(msg)) 100 | } 101 | 102 | override fun isOnline(username: String) = (FMLCommonHandler.instance() 103 | .minecraftServerInstance.configurationManager.getPlayerByUsername(username) ?: null) != null 104 | 105 | private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? { 106 | return FMLCommonHandler.instance() 107 | .minecraftServerInstance.configurationManager.getPlayerByUsername(gameProfile.name) 108 | } 109 | 110 | private fun profileByUUID(uuid: UUID): GameProfile? = try { 111 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.func_152652_a(uuid) 112 | } catch (e: IllegalArgumentException) { 113 | logger.warn("cannot find profile by uuid $uuid") 114 | null 115 | } 116 | 117 | private fun profileByName(username: String): GameProfile? = try { 118 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getGameProfileForUsername(username) 119 | } catch (e: IllegalArgumentException) { 120 | logger.warn("cannot find profile by username $username") 121 | null 122 | } 123 | 124 | override fun collectPlayers(area: Area): Set { 125 | val players = MinecraftServer.getServer().configurationManager.playerEntityList 126 | .map { it as EntityPlayerMP } 127 | .filter { 128 | (area.allDimensions || area.dimensions.contains(it.dimension)) 129 | && area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt()) 130 | } 131 | return players.map { it.uniqueID }.toSet() 132 | } 133 | 134 | override fun nameToUUID(username: String): UUID? = profileByName(username)?.id 135 | 136 | override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name 137 | 138 | override fun commandSenderFor( 139 | user: String, 140 | env: IBridgeCommand.CommandEnvironment, 141 | op: Boolean 142 | ) = MatterLinkCommandSender(user, env, op) 143 | 144 | override val mcVersion: String = MCVERSION 145 | override val modVersion: String = MODVERSION 146 | override val buildNumber = BUILD_NUMBER 147 | override val forgeVersion = ForgeVersion.getVersion() 148 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/update/UpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package matterlink.update 2 | 3 | import com.github.kittinunf.fuel.core.extensions.cUrlString 4 | import com.github.kittinunf.fuel.httpGet 5 | import kotlinx.coroutines.CoroutineName 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Job 8 | import kotlinx.serialization.list 9 | import matterlink.api.ApiMessage 10 | import matterlink.bridge.MessageHandlerInst 11 | import matterlink.config.cfg 12 | import matterlink.handlers.ChatEvent 13 | import matterlink.handlers.LocationHandler 14 | import matterlink.instance 15 | import matterlink.jenkins.JenkinsServer 16 | import matterlink.logger 17 | import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf 18 | import com.github.kittinunf.result.Result 19 | import kotlinx.serialization.json.JSON 20 | 21 | object UpdateChecker : CoroutineScope { 22 | override val coroutineContext = Job() + CoroutineName("UpdateChacker") 23 | 24 | suspend fun check() { 25 | if (cfg.update.enable) { 26 | run() 27 | } 28 | } 29 | 30 | private suspend fun run() { 31 | if (instance.buildNumber > 0) { 32 | val server = JenkinsServer("https://ci.elytradev.com") 33 | val job = server.getJob("elytra/MatterLink/master", "MatterLink/${instance.modVersion}") 34 | ?: run { 35 | logger.error("failed obtaining job: elytra/MatterLink/master") 36 | return 37 | } 38 | //TODO: add job name to constants at build time 39 | val build = job.lastSuccessfulBuild ?: run { 40 | logger.error("no successful build found") 41 | return 42 | } 43 | with(build) { 44 | when { 45 | number > instance.buildNumber -> { 46 | logger.warn("Mod out of date! New build $number available at $url") 47 | val difference = number - instance.buildNumber 48 | LocationHandler.sendToLocations( 49 | msg = "MatterLink out of date! You are $difference builds behind! Please download new version from $url", 50 | x = 0, y = 0, z = 0, dimension = null, 51 | event = ChatEvent.STATUS, 52 | cause = "MatterLink update notice" 53 | ) 54 | } 55 | number < instance.buildNumber -> logger.error("lastSuccessfulBuild: $number is older than installed build: ${instance.buildNumber}") 56 | else -> logger.info("you are up to date") 57 | } 58 | } 59 | return 60 | } 61 | if (instance.modVersion.contains("-dev")) { 62 | logger.debug("Not checking updates on developer build") 63 | return 64 | } 65 | 66 | logger.info("Checking for new versions...") 67 | val (request, response, result) = with(instance) { 68 | val useragent = 69 | "MatterLink/$modVersion MinecraftForge/$mcVersion-$forgeVersion (https://github.com/elytra/MatterLink)" 70 | logger.debug("setting User-Agent: '$useragent'") 71 | 72 | "https://curse.nikky.moe/api/addon/287323/files".httpGet() 73 | .header("User-Agent" to useragent) 74 | .responseObject(kotlinxDeserializerOf(loader = CurseFile.serializer().list, json = JSON.nonstrict)) 75 | } 76 | 77 | val apiUpdateList = when(result) { 78 | is Result.Success -> { 79 | result.value 80 | } 81 | is Result.Failure -> { 82 | logger.error("Could not check for updates!") 83 | logger.error("cUrl: ${request.cUrlString()}") 84 | logger.error("request: $request") 85 | logger.error("response: $response") 86 | return 87 | } 88 | } 89 | .filter { it.fileStatus == "SemiNormal" && it.gameVersion.contains(instance.mcVersion) } 90 | 91 | val modVersionChunks = instance.modVersion 92 | .substringBefore("-dev") 93 | .substringBefore("-build") 94 | .split('.') 95 | .map { 96 | it.toInt() 97 | } 98 | 99 | val possibleUpdates = mutableListOf() 100 | apiUpdateList.forEach { 101 | logger.debug(it.toString()) 102 | val version = it.fileName.substringAfterLast("-").split('.').map { it.toInt() } 103 | var bigger = false 104 | version.forEachIndexed { index, chunk -> 105 | if (!bigger) { 106 | val currentChunk = modVersionChunks.getOrNull(index) ?: 0 107 | logger.debug("$chunk > $currentChunk") 108 | if (chunk < currentChunk) 109 | return@forEach 110 | 111 | bigger = chunk > currentChunk 112 | } 113 | } 114 | if (bigger) { 115 | possibleUpdates += it 116 | } 117 | } 118 | if (possibleUpdates.isEmpty()) return 119 | val latest = possibleUpdates[0] 120 | 121 | possibleUpdates.sortByDescending { it.fileName.substringAfter(" ") } 122 | val count = possibleUpdates.count() 123 | val version = if (count == 1) "version" else "versions" 124 | 125 | logger.info("Matterlink out of date! You are $count $version behind") 126 | possibleUpdates.forEach { 127 | logger.info("version: ${it.fileName} download: ${it.downloadUrl}") 128 | } 129 | 130 | logger.warn("Mod out of date! New $version available at ${latest.downloadUrl}") 131 | MessageHandlerInst.transmit( 132 | ApiMessage( 133 | text = "MatterLink out of date! You are $count $version behind! Please download new version from ${latest.downloadUrl}" 134 | ) 135 | ) 136 | } 137 | } -------------------------------------------------------------------------------- /1.9.4/src/main/kotlin/matterlink/MatterLink.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import com.mojang.authlib.GameProfile 4 | import kotlinx.coroutines.runBlocking 5 | import matterlink.bridge.command.IBridgeCommand 6 | import matterlink.command.AuthCommand 7 | import matterlink.command.MatterLinkCommand 8 | import matterlink.command.MatterLinkCommandSender 9 | import matterlink.config.BaseConfig 10 | import matterlink.config.cfg 11 | import net.minecraft.entity.player.EntityPlayerMP 12 | import net.minecraft.util.text.TextComponentString 13 | import net.minecraftforge.common.ForgeVersion 14 | import net.minecraftforge.common.MinecraftForge 15 | import net.minecraftforge.fml.common.FMLCommonHandler 16 | import net.minecraftforge.fml.common.Mod 17 | import net.minecraftforge.fml.common.event.FMLInitializationEvent 18 | import net.minecraftforge.fml.common.event.FMLPreInitializationEvent 19 | import net.minecraftforge.fml.common.event.FMLServerStartingEvent 20 | import net.minecraftforge.fml.common.event.FMLServerStoppingEvent 21 | import java.util.UUID 22 | 23 | @Mod( 24 | modid = MODID, 25 | name = NAME, version = MODVERSION, 26 | serverSideOnly = true, 27 | useMetadata = true, 28 | acceptableRemoteVersions = "*", 29 | modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter", 30 | dependencies = DEPENDENCIES 31 | ) 32 | object MatterLink : IMatterLink() { 33 | init { 34 | instance = this 35 | } 36 | 37 | @Mod.EventHandler 38 | fun preInit(event: FMLPreInitializationEvent) { 39 | MinecraftForge.EVENT_BUS.register(EventHandler) 40 | logger = with(event.modLog) { 41 | object : Logger { 42 | override fun info(message: String) = this@with.info(message) 43 | override fun debug(message: String) = this@with.debug(message) 44 | override fun error(message: String) = this@with.error(message) 45 | override fun warn(message: String) = this@with.warn(message) 46 | override fun trace(message: String) = this@with.trace(message) 47 | } 48 | } 49 | 50 | logger.info("Building bridge!") 51 | 52 | cfg = BaseConfig(event.modConfigurationDirectory).load() 53 | } 54 | 55 | @Mod.EventHandler 56 | fun init(event: FMLInitializationEvent) { 57 | this.registerBridgeCommands() 58 | } 59 | 60 | @Mod.EventHandler 61 | fun serverStarting(event: FMLServerStartingEvent) = runBlocking { 62 | logger.debug("Registering server commands") 63 | event.registerServerCommand(MatterLinkCommand) 64 | event.registerServerCommand(AuthCommand) 65 | start() 66 | } 67 | 68 | @Mod.EventHandler 69 | fun serverStopping(event: FMLServerStoppingEvent) = runBlocking { 70 | stop() 71 | } 72 | 73 | //FORGE-DEPENDENT 74 | override fun wrappedSendToPlayers(msg: String) { 75 | FMLCommonHandler.instance().minecraftServerInstance.playerList.sendChatMsg(TextComponentString(msg)) 76 | } 77 | 78 | override fun wrappedSendToPlayer(username: String, msg: String) { 79 | val profile = profileByName(username) ?: run { 80 | error("cannot find player by name $username") 81 | return 82 | } 83 | val player = playerByProfile(profile) ?: run { 84 | error("${profile.name} is not online") 85 | return 86 | } 87 | player.addChatMessage(TextComponentString(msg)) 88 | } 89 | 90 | override fun wrappedSendToPlayer(uuid: UUID, msg: String) { 91 | val profile = profileByUUID(uuid) ?: run { 92 | logger.error("cannot find player by uuid $uuid") 93 | return 94 | } 95 | val player = playerByProfile(profile) ?: run { 96 | logger.error("${profile.name} is not online") 97 | return 98 | } 99 | player.addChatMessage(TextComponentString(msg)) 100 | } 101 | 102 | override fun isOnline(username: String) = 103 | FMLCommonHandler.instance().minecraftServerInstance.playerList.allUsernames.contains(username) 104 | 105 | private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? = 106 | FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id) 107 | 108 | 109 | private fun profileByUUID(uuid: UUID): GameProfile? = try { 110 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getProfileByUUID(uuid) 111 | } catch (e: IllegalArgumentException) { 112 | logger.warn("cannot find profile by uuid $uuid") 113 | null 114 | } 115 | 116 | private fun profileByName(username: String): GameProfile? = try { 117 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getGameProfileForUsername(username) 118 | } catch (e: IllegalArgumentException) { 119 | logger.warn("cannot find profile by username $username") 120 | null 121 | } 122 | 123 | override fun collectPlayers(area: Area): Set { 124 | val playerList = FMLCommonHandler.instance().minecraftServerInstance.playerList 125 | val players = playerList.allProfiles 126 | .map { playerList.getPlayerByUUID(it.id) } 127 | .filter { 128 | (area.allDimensions || area.dimensions.contains(it.dimension)) 129 | && area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt()) 130 | } 131 | return players.map { it.uniqueID }.toSet() 132 | } 133 | 134 | override fun nameToUUID(username: String): UUID? = profileByName(username)?.id 135 | 136 | override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name 137 | 138 | override fun commandSenderFor( 139 | user: String, 140 | env: IBridgeCommand.CommandEnvironment, 141 | op: Boolean 142 | ) = MatterLinkCommandSender(user, env, op) 143 | 144 | override val mcVersion: String = MCVERSION 145 | override val modVersion: String = MODVERSION 146 | override val buildNumber = BUILD_NUMBER 147 | override val forgeVersion = ForgeVersion.getVersion() 148 | } 149 | -------------------------------------------------------------------------------- /1.12.2/src/main/kotlin/matterlink/MatterLink.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import com.mojang.authlib.GameProfile 4 | import kotlinx.coroutines.runBlocking 5 | import matterlink.bridge.command.IBridgeCommand 6 | import matterlink.command.AuthCommand 7 | import matterlink.command.MatterLinkCommand 8 | import matterlink.command.MatterLinkCommandSender 9 | import matterlink.config.BaseConfig 10 | import matterlink.config.cfg 11 | import net.minecraft.entity.player.EntityPlayerMP 12 | import net.minecraft.util.text.TextComponentString 13 | import net.minecraftforge.common.ForgeVersion 14 | import net.minecraftforge.fml.common.FMLCommonHandler 15 | import net.minecraftforge.fml.common.Mod 16 | import net.minecraftforge.fml.common.event.FMLInitializationEvent 17 | import net.minecraftforge.fml.common.event.FMLPreInitializationEvent 18 | import net.minecraftforge.fml.common.event.FMLServerStartingEvent 19 | import net.minecraftforge.fml.common.event.FMLServerStoppingEvent 20 | import org.apache.logging.log4j.Level 21 | import org.apache.logging.log4j.core.config.Configurator 22 | import java.util.UUID 23 | 24 | 25 | @Mod( 26 | modid = MODID, 27 | name = NAME, version = MODVERSION, 28 | serverSideOnly = true, 29 | useMetadata = true, 30 | acceptableRemoteVersions = "*", 31 | modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter", 32 | dependencies = DEPENDENCIES 33 | ) 34 | object MatterLink : IMatterLink() { 35 | 36 | init { 37 | Configurator.setLevel(MODID, Level.DEBUG) 38 | instance = this 39 | } 40 | 41 | @Mod.EventHandler 42 | fun preInit(event: FMLPreInitializationEvent) { 43 | logger = with(event.modLog) { 44 | object : Logger { 45 | override fun info(message: String) = this@with.info(message) 46 | override fun debug(message: String) = this@with.debug(message) 47 | override fun error(message: String) = this@with.error(message) 48 | override fun warn(message: String) = this@with.warn(message) 49 | override fun trace(message: String) = this@with.trace(message) 50 | } 51 | } 52 | 53 | logger.info("Building bridge!") 54 | 55 | cfg = BaseConfig(event.modConfigurationDirectory).load() 56 | } 57 | 58 | @Mod.EventHandler 59 | fun init(event: FMLInitializationEvent) { 60 | logger.debug("Registering bridge commands") 61 | this.registerBridgeCommands() 62 | } 63 | 64 | @Mod.EventHandler 65 | fun serverStarting(event: FMLServerStartingEvent) { 66 | logger.debug("Registering server commands") 67 | event.registerServerCommand(MatterLinkCommand) 68 | event.registerServerCommand(AuthCommand) 69 | runBlocking { 70 | start() 71 | } 72 | } 73 | 74 | @Mod.EventHandler 75 | fun serverStopping(event: FMLServerStoppingEvent) = runBlocking { 76 | stop() 77 | } 78 | 79 | //FORGE-DEPENDENT 80 | override fun wrappedSendToPlayers(msg: String) { 81 | FMLCommonHandler.instance().minecraftServerInstance.playerList.sendMessage(TextComponentString(msg)) 82 | } 83 | 84 | override fun wrappedSendToPlayer(username: String, msg: String) { 85 | val profile = profileByName(username) ?: run { 86 | logger.error("cannot find player by name $username") 87 | return 88 | } 89 | val player = playerByProfile(profile) ?: run { 90 | logger.error("${profile.name} is not online") 91 | return 92 | } 93 | player.sendMessage(TextComponentString(msg)) 94 | } 95 | 96 | override fun wrappedSendToPlayer(uuid: UUID, msg: String) { 97 | val profile = profileByUUID(uuid) ?: run { 98 | logger.error("cannot find player by uuid $uuid") 99 | return 100 | } 101 | val player = playerByProfile(profile) ?: run { 102 | logger.error("${profile.name} is not online") 103 | return 104 | } 105 | player.sendMessage(TextComponentString(msg)) 106 | } 107 | 108 | override fun isOnline(username: String) = 109 | FMLCommonHandler.instance().minecraftServerInstance.onlinePlayerNames.contains(username) 110 | 111 | private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? = 112 | FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id) 113 | 114 | private fun profileByUUID(uuid: UUID): GameProfile? = try { 115 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getProfileByUUID(uuid) 116 | } catch (e: IllegalArgumentException) { 117 | logger.warn("cannot find profile by uuid $uuid") 118 | null 119 | } 120 | 121 | private fun profileByName(username: String): GameProfile? = try { 122 | FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getGameProfileForUsername(username) 123 | } catch (e: IllegalArgumentException) { 124 | logger.warn("cannot find profile by username $username") 125 | null 126 | } 127 | 128 | override fun collectPlayers(area: Area): Set { 129 | val players = FMLCommonHandler.instance().minecraftServerInstance.playerList.players.filter { 130 | (area.allDimensions || area.dimensions.contains(it.dimension)) 131 | && area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt()) 132 | } 133 | return players.map { it.uniqueID }.toSet() 134 | } 135 | 136 | override fun nameToUUID(username: String): UUID? = profileByName(username)?.id 137 | 138 | override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name 139 | 140 | override fun commandSenderFor( 141 | user: String, 142 | env: IBridgeCommand.CommandEnvironment, 143 | op: Boolean 144 | ) = MatterLinkCommandSender(user, env, op) 145 | 146 | override val mcVersion: String = MCVERSION 147 | override val modVersion: String = MODVERSION 148 | override val buildNumber = BUILD_NUMBER 149 | override val forgeVersion = ForgeVersion.getVersion() 150 | } 151 | -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/Area.kt: -------------------------------------------------------------------------------- 1 | package matterlink 2 | 3 | import blue.endless.jankson.JsonObject 4 | import blue.endless.jankson.impl.SyntaxError 5 | import kotlin.math.sqrt 6 | 7 | fun JsonObject.parseDimensions(): List = getOrPutList("dimensions", emptyList(), "list of dimension ids") 8 | fun JsonObject.parseAllDimensions(): Boolean = getOrDefault("allDimensions", false, "ignores dimension list") 9 | 10 | sealed class Area { 11 | abstract val type: String 12 | abstract val dimensions: List 13 | abstract val allDimensions: Boolean 14 | 15 | abstract fun testInBounds(x: Int, y: Int, z: Int): Boolean 16 | 17 | fun testForDim(dimension: Int?): Boolean { 18 | if (allDimensions) return true 19 | if (dimension == null) return false 20 | return dimensions.contains(dimension) 21 | } 22 | 23 | companion object { 24 | fun parse(jsonObj: JsonObject): Area { 25 | val type: String = jsonObj.getOrDefault("type", "INFINITE", "Area type identifier") 26 | return when (type.toUpperCase()) { 27 | "INFINITE" -> Infinite.parse(jsonObj) 28 | "RADIUS" -> Radius.parse(jsonObj) 29 | "SPHERE" -> Sphere.parse(jsonObj) 30 | "BOX" -> Box.parse(jsonObj) 31 | "SQUARE" -> Square.parse(jsonObj) 32 | else -> throw SyntaxError("no Area type '$type' found") 33 | } 34 | } 35 | } 36 | 37 | data class Infinite( 38 | override val dimensions: List = listOf(), 39 | override val allDimensions: Boolean = false 40 | ) : Area() { 41 | override val type = "INFINITE" 42 | 43 | override fun testInBounds(x: Int, y: Int, z: Int): Boolean { 44 | return true 45 | } 46 | 47 | companion object { 48 | fun parse(jsonObj: JsonObject): Area { 49 | return Infinite( 50 | dimensions = jsonObj.parseDimensions(), 51 | allDimensions = jsonObj.parseAllDimensions() 52 | ) 53 | } 54 | } 55 | 56 | } 57 | 58 | data class Radius( 59 | override val dimensions: List = listOf(), 60 | override val allDimensions: Boolean = false, 61 | val x: Int, 62 | val z: Int, 63 | val radius: Int? 64 | ) : Area() { 65 | override val type = "RADIUS" 66 | 67 | override fun testInBounds(x: Int, y: Int, z: Int): Boolean { 68 | if (radius == null) return true 69 | return sqrt(((this.x - x) * (this.x - x)) + ((this.z - z) * (this.z - z)).toFloat()) < this.radius 70 | } 71 | 72 | companion object { 73 | fun parse(jsonObj: JsonObject): Area { 74 | 75 | return Radius( 76 | dimensions = jsonObj.parseDimensions(), 77 | allDimensions = jsonObj.parseAllDimensions(), 78 | x = jsonObj.getOrDefault("x", 0), 79 | z = jsonObj.getOrDefault("z", 0), 80 | radius = jsonObj.getReified("radius") 81 | ) 82 | } 83 | } 84 | } 85 | 86 | class Sphere( 87 | override val dimensions: List = listOf(), 88 | override val allDimensions: Boolean = false, 89 | val x: Int, 90 | val y: Int, 91 | val z: Int, 92 | val radius: Int? = null 93 | ) : Area() { 94 | override val type = "SPHERE" 95 | 96 | override fun testInBounds(x: Int, y: Int, z: Int): Boolean { 97 | if (radius == null) return true 98 | return sqrt(((this.x - x) * (this.x - x)) + ((this.y - y) * (this.y - y)) + ((this.z - z) * (this.z - z)).toFloat()) < this.radius 99 | } 100 | 101 | companion object { 102 | fun parse(jsonObj: JsonObject): Area { 103 | 104 | return Sphere( 105 | dimensions = jsonObj.parseDimensions(), 106 | allDimensions = jsonObj.parseAllDimensions(), 107 | x = jsonObj.getOrDefault("x", 0), 108 | y = jsonObj.getOrDefault("y", 0), 109 | z = jsonObj.getOrDefault("z", 0), 110 | radius = jsonObj.getReified("radius") 111 | ) 112 | } 113 | } 114 | } 115 | 116 | class Box( 117 | override val dimensions: List = listOf(), 118 | override val allDimensions: Boolean = false, 119 | val x1: Int, 120 | val x2: Int, 121 | val y1: Int, 122 | val y2: Int, 123 | val z1: Int, 124 | val z2: Int 125 | ) : Area() { 126 | override val type = "BOX" 127 | 128 | override fun testInBounds(x: Int, y: Int, z: Int): Boolean { 129 | return x in x1..x2 && y in y1..y2 && z in z1..z2 130 | } 131 | 132 | companion object { 133 | fun parse(jsonObj: JsonObject): Area { 134 | 135 | return Box( 136 | dimensions = jsonObj.parseDimensions(), 137 | allDimensions = jsonObj.parseAllDimensions(), 138 | x1 = jsonObj.getOrDefault("x1", 0), 139 | x2 = jsonObj.getOrDefault("x2", 0), 140 | y1 = jsonObj.getOrDefault("y1", 0), 141 | y2 = jsonObj.getOrDefault("y2", 0), 142 | z1 = jsonObj.getOrDefault("z1", 0), 143 | z2 = jsonObj.getOrDefault("z2", 0) 144 | ) 145 | } 146 | } 147 | } 148 | 149 | class Square( 150 | override val dimensions: List = listOf(), 151 | override val allDimensions: Boolean = false, 152 | val x1: Int, 153 | val x2: Int, 154 | val z1: Int, 155 | val z2: Int 156 | ) : Area() { 157 | override val type = "SQUARE" 158 | 159 | override fun testInBounds(x: Int, y: Int, z: Int): Boolean { 160 | return x in x1..x2 && z in z1..z2 161 | } 162 | 163 | companion object { 164 | fun parse(jsonObj: JsonObject): Area { 165 | 166 | return Square( 167 | dimensions = jsonObj.parseDimensions(), 168 | allDimensions = jsonObj.parseAllDimensions(), 169 | x1 = jsonObj.getOrDefault("x1", 0), 170 | x2 = jsonObj.getOrDefault("x2", 0), 171 | z1 = jsonObj.getOrDefault("z1", 0), 172 | z2 = jsonObj.getOrDefault("z2", 0) 173 | ) 174 | } 175 | } 176 | } 177 | // 178 | // 179 | // class FakePlayer ( 180 | // val x: Int, 181 | // val y: Int, 182 | // val z: Int, 183 | // val name: String 184 | // ): Area() 185 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/config/CommandConfig.kt: -------------------------------------------------------------------------------- 1 | package matterlink.config 2 | 3 | import blue.endless.jankson.Jankson 4 | import blue.endless.jankson.JsonObject 5 | import blue.endless.jankson.JsonPrimitive 6 | import blue.endless.jankson.impl.SyntaxError 7 | import matterlink.bridge.command.CommandType 8 | import matterlink.bridge.command.CustomCommand 9 | import matterlink.getOrDefault 10 | import matterlink.logger 11 | import matterlink.registerPrimitiveTypeAdapter 12 | import matterlink.registerTypeAdapter 13 | import java.io.File 14 | import java.io.FileNotFoundException 15 | 16 | typealias CommandMap = MutableMap 17 | typealias DefaultCommands = Map> 18 | 19 | object CommandConfig { 20 | private val configFile: File = baseCfg.cfgDirectory.resolve("commands.hjson") 21 | 22 | private val default: DefaultCommands = mapOf( 23 | "tps" to ("""Your run off the mill tps commands, change it to /sampler tps or /cofh tps if you like 24 | |make sure to disable defaultCommand if you want your edits to have any effect 25 | """.trimMargin() 26 | to CustomCommand( 27 | type = CommandType.EXECUTE, 28 | execute = "forge tps", 29 | help = "Print platform tps", 30 | timeout = 200, 31 | defaultCommand = true 32 | )), 33 | "list" to ("lists all the players, this is just a straight pass-through" 34 | to CustomCommand( 35 | type = CommandType.EXECUTE, 36 | execute = "list", 37 | help = "List online players", 38 | defaultCommand = true 39 | )), 40 | "seed" to ("another straight pass-through" 41 | to CustomCommand( 42 | type = CommandType.EXECUTE, 43 | execute = "seed", 44 | help = "Print platform world seed", 45 | defaultCommand = true 46 | )), 47 | "uptime" to ("this is a reponse command, it uses the uptime function, time since the mod was first loaded" 48 | to CustomCommand( 49 | type = CommandType.RESPONSE, 50 | response = "{uptime}", 51 | help = "Print platform uptime", 52 | defaultCommand = true 53 | )), 54 | "whoami" to ("this shows you some of the other response macros" 55 | to CustomCommand( 56 | type = CommandType.RESPONSE, 57 | response = "name: `{user}` userid: `{userid}` platform: `{platform}` username: `{username}` uuid: `{uuid}`", 58 | help = "Print debug user data", 59 | timeout = 200, 60 | defaultCommand = true 61 | )), 62 | "version" to ("are you out of date huh ?" 63 | to CustomCommand( 64 | type = CommandType.RESPONSE, 65 | response = "{version}", 66 | help = "are you out of date huh ?", 67 | timeout = 200, 68 | defaultCommand = true 69 | )), 70 | "exec" to ("this uses arguments in a passed-through command, you could restrict the arguments with a regex" 71 | to CustomCommand( 72 | type = CommandType.EXECUTE, 73 | execute = "{args}", 74 | argumentsRegex = ".*".toRegex(), 75 | permLevel = 50.0, 76 | help = "Execute any command as OP, be careful with this one", 77 | execOp = true, 78 | defaultCommand = true 79 | )) 80 | ) 81 | 82 | val commands: CommandMap = hashMapOf() 83 | 84 | fun loadFile() { 85 | val jankson = Jankson 86 | .builder() 87 | .registerTypeAdapter { jsonObj -> 88 | with(CustomCommand.DEFAULT) { 89 | CustomCommand( 90 | type = jsonObj.get(CommandType::class.java, "type") ?: type, 91 | execute = jsonObj.get(String::class.java, "execute") ?: execute, 92 | response = jsonObj.get(String::class.java, "response") ?: response, 93 | permLevel = jsonObj.get(Double::class.java, "permLevel") ?: permLevel, 94 | help = jsonObj.get(String::class.java, "help") ?: help, 95 | timeout = jsonObj.get(Int::class.java, "timeout") ?: timeout, 96 | defaultCommand = jsonObj.get(Boolean::class.java, "defaultCommand") ?: defaultCommand, 97 | execOp = jsonObj.get(Boolean::class.java, "execOp") ?: execOp, 98 | argumentsRegex = jsonObj.get(Regex::class.java, "argumentsRegex") ?: argumentsRegex 99 | ) 100 | } 101 | } 102 | .registerPrimitiveTypeAdapter { 103 | it.toString().toRegex() 104 | } 105 | .build() 106 | 107 | jankson.marshaller.registerSerializer(Regex::class.java) { regex, _ -> 108 | JsonPrimitive(regex.pattern) 109 | } 110 | 111 | val jsonObject = try { 112 | jankson.load(configFile) 113 | } catch (e: SyntaxError) { 114 | logger.error("error parsing config: ${e.completeMessage}") 115 | JsonObject() 116 | } catch (e: FileNotFoundException) { 117 | configFile.createNewFile() 118 | JsonObject() 119 | } 120 | // clear commands 121 | commands.clear() 122 | jsonObject.forEach { key, element -> 123 | logger.trace("loading command '$key'") 124 | val command = jsonObject.get(CustomCommand::class.java, key) 125 | if (command != null) 126 | commands[key] = command 127 | else { 128 | logger.error("could not parse key: $key, value: '$element' as CustomCommand") 129 | logger.error("skipping $key") 130 | } 131 | } 132 | 133 | //apply defaults 134 | default.forEach { k, (comment, defCommand) -> 135 | val command = commands[k] 136 | if (command == null || command.defaultCommand == true) { 137 | commands[k] = defCommand 138 | jsonObject.getOrDefault(k, defCommand, comment) 139 | } 140 | } 141 | 142 | logger.debug("loaded jsonObj: $jsonObject") 143 | logger.debug("loaded commandMap: $commands") 144 | 145 | val defaultJsonObject = jankson.marshaller.serialize(CustomCommand.DEFAULT) as JsonObject 146 | val nonDefaultJsonObj = jsonObject.clone() 147 | jsonObject.forEach { key, element -> 148 | if (element is JsonObject) { 149 | nonDefaultJsonObj[key] = element.getDelta(defaultJsonObject) 150 | } 151 | } 152 | configFile.writeText(nonDefaultJsonObj.toJson(true, true)) 153 | } 154 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/matterlink/api/MessageHandler.kt: -------------------------------------------------------------------------------- 1 | package matterlink.api 2 | 3 | import com.github.kittinunf.fuel.core.FuelManager 4 | import com.github.kittinunf.fuel.core.Method 5 | import com.github.kittinunf.fuel.core.ResponseDeserializable 6 | import com.github.kittinunf.fuel.core.extensions.cUrlString 7 | import com.github.kittinunf.fuel.core.extensions.jsonBody 8 | import com.github.kittinunf.fuel.coroutines.awaitStringResponseResult 9 | import com.github.kittinunf.fuel.httpGet 10 | import com.github.kittinunf.fuel.httpPost 11 | import com.github.kittinunf.result.Result 12 | import kotlinx.coroutines.CoroutineName 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi 16 | import kotlinx.coroutines.Job 17 | import kotlinx.coroutines.ObsoleteCoroutinesApi 18 | import kotlinx.coroutines.channels.BroadcastChannel 19 | import kotlinx.coroutines.channels.Channel 20 | import kotlinx.coroutines.channels.SendChannel 21 | import kotlinx.coroutines.channels.actor 22 | import kotlinx.coroutines.channels.broadcast 23 | import kotlinx.coroutines.channels.consumeEach 24 | import kotlinx.coroutines.delay 25 | import kotlinx.coroutines.isActive 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.runBlocking 28 | import kotlinx.serialization.json.JSON 29 | import kotlinx.serialization.list 30 | import matterlink.Logger 31 | import java.io.Reader 32 | import java.net.ConnectException 33 | import kotlin.coroutines.CoroutineContext 34 | 35 | /** 36 | * Created by nikky on 07/05/18. 37 | * 38 | * @author Nikky 39 | * @version 1.0 40 | */ 41 | open class MessageHandler : CoroutineScope { 42 | override val coroutineContext: CoroutineContext = Job() 43 | private var enabled = false 44 | 45 | private var connectErrors = 0 46 | private var reconnectCooldown = 0L 47 | private var sendErrors = 0 48 | 49 | private var sendChannel: SendChannel = senderActor() 50 | 51 | private val messageStream = Channel(Channel.UNLIMITED) 52 | @UseExperimental(ExperimentalCoroutinesApi::class) 53 | var broadcast: BroadcastChannel = broadcast { 54 | while (true) { 55 | val msg = messageStream.receive() 56 | send(msg) 57 | } 58 | } 59 | private set 60 | private val keepOpenManager = FuelManager().apply { 61 | timeoutInMillisecond = 0 62 | timeoutReadInMillisecond = 0 63 | } 64 | 65 | var config: Config = Config() 66 | 67 | var logger = object : Logger { 68 | override fun info(message: String) = println("INFO: $message") 69 | override fun debug(message: String) = println("DEBUG: $message") 70 | override fun error(message: String) = println("ERROR: $message") 71 | override fun warn(message: String) = println("WARN: $message") 72 | override fun trace(message: String) = println("TRACE: $message") 73 | } 74 | 75 | suspend fun stop(message: String? = null) { 76 | if (message != null && config.announceDisconnect) { 77 | sendStatusUpdate(message) 78 | } 79 | enabled = false 80 | rcvJob?.cancel() 81 | rcvJob = null 82 | } 83 | 84 | private var rcvJob: Job? = null 85 | 86 | suspend fun start(message: String?, clear: Boolean) { 87 | logger.debug("starting connection") 88 | if (clear) { 89 | clear() 90 | } 91 | 92 | enabled = true 93 | 94 | rcvJob = messageBroadcast() 95 | 96 | if (message != null && config.announceConnect) { 97 | sendStatusUpdate(message) 98 | } 99 | } 100 | 101 | private suspend fun clear() { 102 | val url = "${config.url}/api/messages" 103 | val (request, response, result) = url.httpGet() 104 | .apply { 105 | if (config.token.isNotEmpty()) { 106 | headers["Authorization"] = "Bearer ${config.token}" 107 | } 108 | } 109 | .awaitStringResponseResult() 110 | 111 | when (result) { 112 | is Result.Success -> { 113 | val messages: List = JSON.nonstrict.parse(ApiMessage.list, result.value) 114 | messages.forEach { msg -> 115 | logger.trace("skipping $msg") 116 | } 117 | logger.debug("skipped ${messages.count()} messages") 118 | } 119 | is Result.Failure -> { 120 | logger.error("failed to clear messages") 121 | logger.error("url: $url") 122 | logger.error("cUrl: ${request.cUrlString()}") 123 | logger.error("response: $response") 124 | logger.error(result.error.exception.localizedMessage) 125 | result.error.exception.printStackTrace() 126 | } 127 | } 128 | } 129 | 130 | open suspend fun sendStatusUpdate(message: String) { 131 | transmit(ApiMessage(text = message)) 132 | } 133 | 134 | open suspend fun transmit(msg: ApiMessage) { 135 | // if (streamConnection.isConnected || streamConnection.isConnecting) { 136 | if (msg.username.isEmpty()) 137 | msg.username = config.systemUser 138 | if (msg.gateway.isEmpty()) { 139 | logger.error("missing gateway on message: $msg") 140 | return 141 | } 142 | logger.debug("Transmitting: $msg") 143 | sendChannel.send(msg) 144 | // } 145 | } 146 | 147 | @Deprecated("use coroutine api", level = DeprecationLevel.ERROR) 148 | fun checkConnection() { 149 | } 150 | 151 | @UseExperimental(ObsoleteCoroutinesApi::class) 152 | private fun CoroutineScope.senderActor() = actor(context = Dispatchers.IO) { 153 | consumeEach { 154 | try { 155 | logger.debug("sending $it") 156 | val url = "${config.url}/api/message" 157 | val (request, response, result) = url.httpPost() 158 | .apply { 159 | if (config.token.isNotEmpty()) { 160 | headers["Authorization"] = "Bearer ${config.token}" 161 | } 162 | } 163 | .jsonBody(it.encode()) 164 | .responseString() 165 | when (result) { 166 | is Result.Success -> { 167 | logger.debug("sent $it") 168 | sendErrors = 0 169 | } 170 | is Result.Failure -> { 171 | sendErrors++ 172 | logger.error("failed to deliver: $it") 173 | logger.error("url: $url") 174 | logger.error("cUrl: ${request.cUrlString()}") 175 | logger.error("response: $response") 176 | logger.error(result.error.exception.localizedMessage) 177 | result.error.exception.printStackTrace() 178 | // close() 179 | throw result.error.exception 180 | } 181 | } 182 | } catch (connectError: ConnectException) { 183 | connectError.printStackTrace() 184 | sendErrors++ 185 | } 186 | } 187 | } 188 | 189 | private fun CoroutineScope.messageBroadcast() = launch(context = Dispatchers.IO + CoroutineName("msgBroadcaster")) { 190 | loop@ while (isActive) { 191 | logger.info("opening connection") 192 | val url = "${config.url}/api/stream" 193 | val (request, response, result) = keepOpenManager.request(Method.GET, url) 194 | .apply { 195 | if (config.token.isNotEmpty()) { 196 | headers["Authorization"] = "Bearer ${config.token}" 197 | } 198 | } 199 | .responseObject(object : ResponseDeserializable { 200 | override fun deserialize(reader: Reader) = 201 | runBlocking(Dispatchers.IO + CoroutineName("msgReceiver")) { 202 | logger.info("connected successfully") 203 | connectErrors = 0 204 | reconnectCooldown = 0 205 | 206 | reader.useLines { lines -> 207 | lines.forEach { line -> 208 | val msg = ApiMessage.decode(line) 209 | logger.debug("received: $msg") 210 | if (msg.event != "api_connect") { 211 | messageStream.send(msg) 212 | } 213 | } 214 | } 215 | } 216 | }) 217 | 218 | when (result) { 219 | is Result.Success -> { 220 | logger.info("connection closed") 221 | } 222 | is Result.Failure -> { 223 | connectErrors++ 224 | reconnectCooldown = connectErrors * 1000L 225 | logger.error("connectErrors: $connectErrors") 226 | logger.error("connection error") 227 | logger.error("curl: ${request.cUrlString()}") 228 | logger.error(result.error.localizedMessage) 229 | result.error.exception.printStackTrace() 230 | if (connectErrors >= 10) { 231 | logger.error("Caught too many errors, closing bridge") 232 | stop("Interrupting connection to matterbridge API due to accumulated connection errors") 233 | break@loop 234 | } 235 | } 236 | } 237 | delay(reconnectCooldown) // reconnect delay in ms 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /matterbridge-sample.toml: -------------------------------------------------------------------------------- 1 | #This is configuration for matterbridge. 2 | #WARNING: as this file contains credentials, be sure to set correct file permissions 3 | ################################################################### 4 | #IRC section 5 | ################################################################### 6 | #REQUIRED to start IRC section 7 | [irc] 8 | 9 | #You can configure multiple servers "[irc.name]" or "[irc.name2]" 10 | #In this example we use [irc.freenode] 11 | #REQUIRED 12 | [irc.esper] 13 | #irc server to connect to. 14 | #REQUIRED 15 | Server="irc.esper.net:6697" 16 | 17 | #Password for irc server (if necessary) 18 | #OPTIONAL (default "") 19 | Password="" 20 | 21 | #Enable to use TLS connection to your irc server. 22 | #OPTIONAL (default false) 23 | UseTLS=true 24 | 25 | #Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts) 26 | #It uses NickServNick and NickServPassword as login and password 27 | #OPTIONAL (default false) 28 | UseSASL=false 29 | 30 | #Enable to not verify the certificate on your irc server. i 31 | #e.g. when using selfsigned certificates 32 | #OPTIONAL (default false) 33 | SkipTLSVerify=true 34 | 35 | #If you know your charset, you can specify it manually. 36 | #Otherwise it tries to detect this automatically. Select one below 37 | # "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew", 38 | # "cp932", "iso-8859-15", "cp437", "utf-16be", "iso-8859-3:1988", "windows-1251", "utf16", "latin6", 39 | # "latin3", "iso-8859-1:1987", "iso-8859-9", "utf-16le", "big5", "cp819", "asmo-708", "utf-8", 40 | # "ibm437", "iso-ir-157", "iso-ir-144", "latin4", "850", "iso-8859-5", "iso-8859-5:1988", "l3", 41 | # "windows-31j", "utf8", "iso-8859-3", "437", "greek", "iso-8859-8", "l6", "l9-iso-8859-15", 42 | # "iso-8859-2", "latin2", "iso-ir-100", "iso-8859-6", "arabic", "iso-ir-148", "us-ascii", "x-sjis", 43 | # "utf16be", "iso-8859-8:1988", "utf16le", "l4", "utf-16", "iso-ir-138", "iso-8859-7", "iso-8859-7:1987", 44 | # "windows-1252", "l2", "koi8-r", "iso8859-1", "latin1", "ecma-114", "iso-ir-110", "elot-928", 45 | # "iso-ir-126", "iso-8859-1", "iso-ir-127", "cp850", "cyrillic", "greek8", "windows-1250", "iso-latin-1", 46 | # "l5", "ibm866", "cp866", "ms-kanji", "ibm850", "ecma-118", "iso-ir-101", "ibm819", "l1", "iso-8859-6:1987", 47 | # "latin5", "ascii", "sjis", "iso-8859-10", "iso-8859-4", "iso-8859-4:1988", "shift-jis 48 | # The select charset will be converted to utf-8 when sent to other bridges. 49 | #OPTIONAL (default "") 50 | Charset="" 51 | 52 | #Your nick on irc. 53 | #REQUIRED 54 | Nick="matterbot" 55 | 56 | #If you registered your bot with a service like Nickserv on freenode. 57 | #Also being used when UseSASL=true 58 | # 59 | #Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org" 60 | #OPTIONAL 61 | NickServNick="nickserv" 62 | NickServPassword="secret" 63 | 64 | #OPTIONAL only used for quakenet auth 65 | NickServUsername="username" 66 | 67 | #Flood control 68 | #Delay in milliseconds between each message send to the IRC server 69 | #OPTIONAL (default 1300) 70 | MessageDelay=1300 71 | 72 | #Maximum amount of messages to hold in queue. If queue is full 73 | #messages will be dropped. 74 | # will be add to the message that fills the queue. 75 | #OPTIONAL (default 30) 76 | MessageQueue=30 77 | 78 | #Maximum length of message sent to irc server. If it exceeds 79 | # will be add to the message. 80 | #OPTIONAL (default 400) 81 | MessageLength=400 82 | 83 | #Split messages on MessageLength instead of showing the 84 | #WARNING: this could lead to flooding 85 | #OPTIONAL (default false) 86 | MessageSplit=false 87 | 88 | #Delay in seconds to rejoin a channel when kicked 89 | #OPTIONAL (default 0) 90 | RejoinDelay=0 91 | 92 | #Nicks you want to ignore. 93 | #Messages from those users will not be sent to other bridges. 94 | #OPTIONAL 95 | IgnoreNicks="ircspammer1 ircspammer2" 96 | 97 | #Messages you want to ignore. 98 | #Messages matching these regexp will be ignored and not sent to other bridges 99 | #See https://regex-golang.appspot.com/assets/html/index.html for more regex info 100 | #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword 101 | IgnoreMessages="^~~ badword" 102 | 103 | #messages you want to replace. 104 | #it replaces outgoing messages from the bridge. 105 | #so you need to place it by the sending bridge definition. 106 | #regular expressions supported 107 | #some examples: 108 | #this replaces cat => dog and sleep => awake 109 | #replacemessages=[ ["cat","dog"], ["sleep","awake"] ] 110 | #this replaces every number with number. 123 => numbernumbernumber 111 | #replacemessages=[ ["[0-9]","number"] ] 112 | #optional (default empty) 113 | ReplaceMessages=[ ["cat","dog"] ] 114 | 115 | #nicks you want to replace. 116 | #see replacemessages for syntaxa 117 | #optional (default empty) 118 | ReplaceNicks=[ ["user--","user"] ] 119 | 120 | #extra label that can be used in the RemoteNickFormat 121 | #optional (default empty) 122 | Label="" 123 | 124 | #RemoteNickFormat defines how remote users appear on this bridge 125 | #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. 126 | #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge 127 | #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge 128 | #The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information 129 | #OPTIONAL (default empty) 130 | RemoteNickFormat="[{PROTOCOL}.{BRIDGE}] <{NOPINGNICK}> " 131 | 132 | #Enable to show users joins/parts from other bridges 133 | #Currently works for messages from the following bridges: irc, mattermost, slack 134 | #OPTIONAL (default false) 135 | ShowJoinPart=true 136 | 137 | #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 138 | #It will strip other characters from the nick 139 | #OPTIONAL (default false) 140 | StripNick=false 141 | 142 | #Enable to show topic changes from other bridges 143 | #Only works hiding/show topic changes from slack bridge for now 144 | #OPTIONAL (default false) 145 | ShowTopicChange=false 146 | 147 | ################################################################### 148 | #API 149 | ################################################################### 150 | [api] 151 | #You can configure multiple API hooks 152 | #In this example we use [api.minecraft] 153 | #REQUIRED 154 | 155 | [api.minecraft] 156 | #Address to listen on for API 157 | #REQUIRED 158 | BindAddress="127.0.0.1:4242" 159 | 160 | #Amount of messages to keep in memory 161 | Buffer=1000 162 | 163 | #Bearer token used for authentication 164 | #curl -H "Authorization: Bearer token" http://localhost:4242/api/messages 165 | #OPTIONAL (no authorization if token is empty) 166 | Token="" 167 | 168 | #extra label that can be used in the RemoteNickFormat 169 | #optional (default empty) 170 | Label="minecraft" 171 | 172 | #RemoteNickFormat defines how remote users appear on this bridge 173 | #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. 174 | #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge 175 | #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge 176 | #OPTIONAL (default empty) 177 | RemoteNickFormat="{NICK}" 178 | 179 | ################################################################### 180 | #General configuration 181 | ################################################################### 182 | # Settings here are defaults that each protocol can override 183 | [general] 184 | #RemoteNickFormat defines how remote users appear on this bridge 185 | #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. 186 | #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge 187 | #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge 188 | #OPTIONAL (default empty) 189 | RemoteNickFormat="[{PROTOCOL}] <{NICK}> " 190 | 191 | #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 192 | #It will strip other characters from the nick 193 | #OPTIONAL (default false) 194 | StripNick=false 195 | 196 | 197 | #MediaServerUpload and MediaServerDownload are used for uploading images/files/video to 198 | #a remote "mediaserver" (a webserver like caddy for example). 199 | #When configured images/files uploaded on bridges like mattermost,slack, telegram will be downloaded 200 | #and uploaded again to MediaServerUpload URL 201 | #The MediaServerDownload will be used so that bridges without native uploading support: 202 | #gitter, irc and xmpp will be shown links to the files on MediaServerDownload 203 | # 204 | #More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D 205 | #OPTIONAL (default empty) 206 | MediaServerUpload="https://user:pass@yourserver.com/upload" 207 | #OPTIONAL (default empty) 208 | MediaServerDownload="https://youserver.com/download" 209 | 210 | #MediaDownloadSize is the maximum size of attachments, videos, images 211 | #matterbridge will download and upload this file to bridges that also support uploading files. 212 | #eg downloading from slack to upload it to mattermost 213 | # 214 | #It will only download from bridges that don't have public links available, which are for the moment 215 | #slack, telegram, matrix and mattermost 216 | # 217 | #Optional (default 1000000 (1 megabyte)) 218 | MediaDownloadSize=1000000 219 | 220 | ################################################################### 221 | #Gateway configuration 222 | ################################################################### 223 | 224 | #You can specify multiple gateways using [[gateway]] 225 | #Each gateway has a [[gateway.in]] and a [[gateway.out]] 226 | #[[gateway.in]] specifies the account and channels we will receive messages from. 227 | #[[gateway.out]] specifies the account and channels we will send the messages 228 | #from [[gateway.in]] to. 229 | # 230 | #Most of the time [[gateway.in]] and [[gateway.out]] are the same if you 231 | #want bidirectional bridging. You can then use [[gateway.inout]] 232 | # 233 | 234 | [[gateway]] 235 | #REQUIRED and UNIQUE 236 | name="minecraft" 237 | #Enable enables this gateway 238 | ##OPTIONAL (default false) 239 | enable=true 240 | 241 | # API 242 | [[gateway.inout]] 243 | account="api.minecraft" 244 | channel="api" 245 | 246 | [[gateway.inout]] 247 | account="irc.esper" 248 | channel="#matterlink" --------------------------------------------------------------------------------