Merge branch 'scala'
# Conflicts: # pom.xml # src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java # src/main/scala/buttondevteam/discordplugin/ChromaBot.java # src/main/scala/buttondevteam/discordplugin/ChromaBot.scala # src/main/scala/buttondevteam/discordplugin/DiscordPlugin.java # src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala # src/main/scala/buttondevteam/discordplugin/listeners/CommandListener.java
This commit is contained in:
commit
6183c034c6
102 changed files with 4730 additions and 5568 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -223,3 +223,4 @@ TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
|
||||||
*.xml
|
*.xml
|
||||||
|
|
||||||
Token.txt
|
Token.txt
|
||||||
|
.bsp
|
||||||
|
|
141
build.sbt
Normal file
141
build.sbt
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import scala.io.Source
|
||||||
|
import scala.util.Using
|
||||||
|
|
||||||
|
name := "Chroma-Discord"
|
||||||
|
|
||||||
|
version := "1.1"
|
||||||
|
|
||||||
|
scalaVersion := "3.0.0"
|
||||||
|
|
||||||
|
resolvers += "spigot-repo" at "https://hub.spigotmc.org/nexus/content/repositories/snapshots/"
|
||||||
|
resolvers += "jitpack.io" at "https://jitpack.io"
|
||||||
|
resolvers += Resolver.mavenLocal
|
||||||
|
|
||||||
|
libraryDependencies ++= Seq(
|
||||||
|
"org.spigotmc" % "spigot-api" % "1.12.2-R0.1-SNAPSHOT" % Provided,
|
||||||
|
"org.spigotmc" % "spigot" % "1.12.2-R0.1-SNAPSHOT" % Provided,
|
||||||
|
"org.spigotmc." % "spigot" % "1.14.4-R0.1-SNAPSHOT" % Provided,
|
||||||
|
"com.destroystokyo.paper" % "paper" % "1.16.3-R0.1-SNAPSHOT" % Provided,
|
||||||
|
|
||||||
|
"com.discord4j" % "discord4j-core" % "3.2.1",
|
||||||
|
"org.slf4j" % "slf4j-jdk14" % "1.7.32",
|
||||||
|
"com.vdurmont" % "emoji-java" % "5.1.1",
|
||||||
|
"org.mockito" % "mockito-core" % "4.2.0",
|
||||||
|
"io.projectreactor" % "reactor-scala-extensions_2.13" % "0.8.0",
|
||||||
|
// https://mvnrepository.com/artifact/org.immutables/value
|
||||||
|
"org.immutables" % "value" % "2.8.8" % "provided",
|
||||||
|
|
||||||
|
"com.github.TBMCPlugins.ChromaCore" % "Chroma-Core" % "v1.0.0" % Provided,
|
||||||
|
"net.ess3" % "EssentialsX" % "2.17.1" % Provided,
|
||||||
|
"net.luckperms" % "api" % "5.3" % Provided,
|
||||||
|
)
|
||||||
|
|
||||||
|
assembly / assemblyJarName := "Chroma-Discord.jar"
|
||||||
|
assembly / assemblyShadeRules := Seq(
|
||||||
|
"io.netty", "com.fasterxml", "org.mockito", "org.slf4j"
|
||||||
|
).map { p =>
|
||||||
|
ShadeRule.rename(s"$p.**" -> "btndvtm.dp.@0").inAll
|
||||||
|
}
|
||||||
|
|
||||||
|
assembly / assemblyMergeStrategy := {
|
||||||
|
case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.concat
|
||||||
|
// https://stackoverflow.com/a/55557287/457612
|
||||||
|
case "module-info.class" => MergeStrategy.discard
|
||||||
|
case x => (assembly / assemblyMergeStrategy).value(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveConfigComments = TaskKey[Seq[File]]("saveConfigComments")
|
||||||
|
saveConfigComments := {
|
||||||
|
val sv = (Compile / sources).value
|
||||||
|
val cdataRegex = Pattern.compile("(?:def|val|var) (\\w+)(?::[^=]+)? = (?:getI?Config|DPUtils.\\w+Data)") //Hack: DPUtils
|
||||||
|
val clRegex = Pattern.compile("class (\\w+).* extends ((?:\\w|\\d)+)")
|
||||||
|
val objRegex = Pattern.compile("object (\\w+)")
|
||||||
|
val subRegex = Pattern.compile("def `?(\\w+)`?")
|
||||||
|
val subParamRegex = Pattern.compile("((?:\\w|\\d)+): ((?:\\w|\\d)+)")
|
||||||
|
val configConfig = new YamlConfiguration()
|
||||||
|
val commandConfig = new YamlConfiguration()
|
||||||
|
|
||||||
|
def getConfigComments(line: String, clKey: String, comment: String, justCommented: Boolean): (String, String, Boolean) = {
|
||||||
|
val clMatcher = clRegex.matcher(line)
|
||||||
|
if (clKey == null && clMatcher.find()) { //First occurrence
|
||||||
|
(if (clMatcher.group(2).contains("Component"))
|
||||||
|
"components." + clMatcher.group(1)
|
||||||
|
else "global", comment, justCommented)
|
||||||
|
} else if (line.contains("/**")) {
|
||||||
|
(clKey, "", false)
|
||||||
|
} else if (line.contains("*/") && comment != null)
|
||||||
|
(clKey, comment, true)
|
||||||
|
else if (comment != null) {
|
||||||
|
if (justCommented) {
|
||||||
|
if (clKey != null) {
|
||||||
|
val matcher = cdataRegex.matcher(line)
|
||||||
|
if (matcher.find())
|
||||||
|
configConfig.set(s"$clKey.${matcher.group(1)}", comment.trim)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val matcher = objRegex.matcher(line)
|
||||||
|
if (matcher.find()) {
|
||||||
|
val clName = matcher.group(1)
|
||||||
|
val compKey = if (clName.contains("Module")) s"component.$clName"
|
||||||
|
else if (clName.contains("Plugin")) "global"
|
||||||
|
else null
|
||||||
|
if (compKey != null)
|
||||||
|
configConfig.set(s"$compKey.generalDescriptionInsteadOfAConfig", comment.trim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(clKey, null, false)
|
||||||
|
}
|
||||||
|
else (clKey, comment + line.replaceFirst("^\\s*\\*\\s+", "") + "\n", justCommented)
|
||||||
|
}
|
||||||
|
else (clKey, comment, justCommented)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (file <- sv) {
|
||||||
|
Using(Source.fromFile(file)) { src =>
|
||||||
|
var clKey: String = null
|
||||||
|
var comment: String = null
|
||||||
|
var justCommented: Boolean = false
|
||||||
|
|
||||||
|
var subCommand = false
|
||||||
|
var pkg: String = null
|
||||||
|
var clName: String = null
|
||||||
|
for (line <- src.getLines) {
|
||||||
|
val (clk, c, jc) = getConfigComments(line, clKey, comment, justCommented)
|
||||||
|
clKey = clk; comment = c; justCommented = jc
|
||||||
|
|
||||||
|
val objMatcher = objRegex.matcher(line)
|
||||||
|
val clMatcher = clRegex.matcher(line)
|
||||||
|
if (pkg == null && line.startsWith("package "))
|
||||||
|
pkg = line.substring("package ".length)
|
||||||
|
else if (clName == null && objMatcher.find())
|
||||||
|
clName = objMatcher.group(1)
|
||||||
|
else if (clName == null && clMatcher.find())
|
||||||
|
clName = clMatcher.group(1)
|
||||||
|
val subMatcher = subRegex.matcher(line)
|
||||||
|
val subParamMatcher = subParamRegex.matcher(line)
|
||||||
|
val sub = line.contains("@Subcommand") || line.contains("@Command2.Subcommand")
|
||||||
|
if (sub) subCommand = true
|
||||||
|
else if (line.contains("}")) subCommand = false
|
||||||
|
if (subCommand && subMatcher.find()) {
|
||||||
|
/*val groups = (2 to subMatcher.groupCount()).map(subMatcher.group)
|
||||||
|
val pairs = for (i <- groups.indices by 2) yield (groups(i), groups(i + 1))*/
|
||||||
|
val mname = subMatcher.group(1)
|
||||||
|
val params = Iterator.continually(()).takeWhile(_ => subParamMatcher.find())
|
||||||
|
.map(_ => subParamMatcher.group(1)).drop(1)
|
||||||
|
val section = commandConfig.createSection(s"$pkg.$clName.$mname")
|
||||||
|
section.set("method", s"$mname()")
|
||||||
|
section.set("params", params.mkString(" "))
|
||||||
|
subCommand = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configConfig.save("target/configHelp.yml")
|
||||||
|
commandConfig.save("target/commands.yml")
|
||||||
|
}.recover[Unit]({ case t => t.printStackTrace() })
|
||||||
|
}
|
||||||
|
Seq(file("target/configHelp.yml"), file("target/commands.yml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Compile / resourceGenerators += saveConfigComments
|
226
pom.xml
226
pom.xml
|
@ -1,226 +0,0 @@
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>com.github.TBMCPlugins.ChromaCore</groupId>
|
|
||||||
<artifactId>CorePOM</artifactId>
|
|
||||||
<version>master-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<groupId>com.github.TBMCPlugins</groupId>
|
|
||||||
<artifactId>Chroma-Discord</artifactId>
|
|
||||||
<version>v${noprefix.version}-SNAPSHOT</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>Chroma-Discord</name>
|
|
||||||
<url>http://maven.apache.org</url>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<!-- <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
|
|
||||||
<testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> -->
|
|
||||||
<sourceDirectory>src/main/java</sourceDirectory>
|
|
||||||
<resources>
|
|
||||||
<resource>
|
|
||||||
<directory>src/main/resources</directory>
|
|
||||||
<filtering>true</filtering>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
|
||||||
<finalName>Chroma-Discord</finalName>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.2.1</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<artifactSet>
|
|
||||||
<!-- <includes>
|
|
||||||
<include>com.discord4j:discord4j*</include>
|
|
||||||
<include>com.vdurmont:emoji-java</include>
|
|
||||||
</includes> --> <!-- http://stackoverflow.com/questions/28458058/maven-shade-plugin-exclude-a-dependency-and-all-its-transitive-dependencies -->
|
|
||||||
</artifactSet>
|
|
||||||
<!-- <minimizeJar>true</minimizeJar> Tried using filters but we need pretty much all of that 12 MB -->
|
|
||||||
<relocations>
|
|
||||||
<relocation>
|
|
||||||
<pattern>io.netty</pattern>
|
|
||||||
<shadedPattern>btndvtm.dp.io.netty</shadedPattern>
|
|
||||||
<excludes>
|
|
||||||
</excludes>
|
|
||||||
</relocation>
|
|
||||||
<relocation>
|
|
||||||
<pattern>com.fasterxml</pattern>
|
|
||||||
<shadedPattern>btndvtm.dp.com.fasterxml</shadedPattern>
|
|
||||||
</relocation>
|
|
||||||
<relocation>
|
|
||||||
<pattern>org.mockito</pattern>
|
|
||||||
<shadedPattern>btndvtm.dp.org.mockito</shadedPattern>
|
|
||||||
</relocation>
|
|
||||||
<relocation>
|
|
||||||
<pattern>org.slf4j</pattern>
|
|
||||||
<shadedPattern>btndvtm.dp.org.slf4j</shadedPattern>
|
|
||||||
</relocation>
|
|
||||||
</relocations>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>2.4.2</version>
|
|
||||||
<configuration>
|
|
||||||
<useSystemClassLoader>false
|
|
||||||
</useSystemClassLoader> <!-- https://stackoverflow.com/a/53012553/2703239 -->
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<noprefix.version>1.0.0</noprefix.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>spigot-repo</id>
|
|
||||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
|
||||||
</repository>
|
|
||||||
<repository> <!-- This repo fixes issues with transitive dependencies -->
|
|
||||||
<id>jcenter</id>
|
|
||||||
<url>http://jcenter.bintray.com</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>jitpack.io</id>
|
|
||||||
<url>https://jitpack.io</url>
|
|
||||||
</repository>
|
|
||||||
<!-- <repository>
|
|
||||||
<id>vault-repo</id>
|
|
||||||
<url>http://nexus.hc.to/content/repositories/pub_releases</url>
|
|
||||||
</repository> -->
|
|
||||||
<repository>
|
|
||||||
<id>Essentials</id>
|
|
||||||
<url>https://ci.ender.zone/plugin/repository/everything/</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>projectlombok.org</id>
|
|
||||||
<url>http://projectlombok.org/mavenrepo</url>
|
|
||||||
</repository>
|
|
||||||
<!-- <repository>
|
|
||||||
<id>pex-repo</id>
|
|
||||||
<url>http://pex-repo.aoeu.xyz</url>
|
|
||||||
</repository> -->
|
|
||||||
<!-- <repository>
|
|
||||||
<id>Reactor-Tools</id>
|
|
||||||
<url>https://repo.spring.io/milestone</url>
|
|
||||||
</repository> -->
|
|
||||||
<repository>
|
|
||||||
<id>papermc</id>
|
|
||||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<version>4.13.1</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.spigotmc</groupId>
|
|
||||||
<artifactId>spigot-api</artifactId>
|
|
||||||
<version>1.12.2-R0.1-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.spigotmc</groupId>
|
|
||||||
<artifactId>spigot</artifactId>
|
|
||||||
<version>1.12.2-R0.1-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.spigotmc.</groupId>
|
|
||||||
<artifactId>spigot</artifactId>
|
|
||||||
<version>1.14.4-R0.1-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency> <!-- From patched_1.16.3.jar -->
|
|
||||||
<groupId>com.destroystokyo.paper</groupId>
|
|
||||||
<artifactId>paper</artifactId>
|
|
||||||
<version>1.16.3-R0.1-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<!-- https://mvnrepository.com/artifact/com.discord4j/Discord4J -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.discord4j</groupId>
|
|
||||||
<artifactId>discord4j-core</artifactId>
|
|
||||||
<version>3.1.3</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-jdk14</artifactId>
|
|
||||||
<version>1.7.21</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.TBMCPlugins.ChromaCore</groupId>
|
|
||||||
<artifactId>Chroma-Core</artifactId>
|
|
||||||
<version>v1.0.0</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.ess3</groupId>
|
|
||||||
<artifactId>EssentialsX</artifactId>
|
|
||||||
<version>2.17.1</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.10</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<!-- <dependency>
|
|
||||||
<groupId>ru.tehkode</groupId>
|
|
||||||
<artifactId>PermissionsEx</artifactId>
|
|
||||||
<version>1.23.1</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.bukkit</groupId>
|
|
||||||
<artifactId>bukkit</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency> -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.vdurmont</groupId>
|
|
||||||
<artifactId>emoji-java</artifactId>
|
|
||||||
<version>4.0.0</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- https://mvnrepository.com/artifact/io.projectreactor.tools/blockhound -->
|
|
||||||
<!-- <dependency>
|
|
||||||
<groupId>io.projectreactor.tools</groupId>
|
|
||||||
<artifactId>blockhound</artifactId>
|
|
||||||
<version>1.0.0.M3</version>
|
|
||||||
</dependency> -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.lucko.LuckPerms</groupId>
|
|
||||||
<artifactId>bukkit</artifactId>
|
|
||||||
<version>master-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mockito</groupId>
|
|
||||||
<artifactId>mockito-core</artifactId>
|
|
||||||
<version>3.5.13</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
2
project/build.properties
Normal file
2
project/build.properties
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
sbt.version=1.5.8
|
||||||
|
scala.version=3.0.0
|
6
project/build.sbt
Normal file
6
project/build.sbt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
//lazy val commenter = RootProject(file("../commenter"))
|
||||||
|
//lazy val root = (project in file(".")).dependsOn(commenter)
|
||||||
|
|
||||||
|
resolvers += Resolver.mavenLocal
|
||||||
|
|
||||||
|
libraryDependencies += "org.spigotmc" % "spigot-api" % "1.12.2-R0.1-SNAPSHOT"
|
1
project/plugins.sbt
Normal file
1
project/plugins.sbt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
|
|
@ -1,24 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.util.DPState;
|
|
||||||
import org.apache.logging.log4j.Level;
|
|
||||||
import org.apache.logging.log4j.core.Filter;
|
|
||||||
import org.apache.logging.log4j.core.LogEvent;
|
|
||||||
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
|
||||||
import org.apache.logging.log4j.core.filter.LevelRangeFilter;
|
|
||||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
|
||||||
|
|
||||||
public class BukkitLogWatcher extends AbstractAppender {
|
|
||||||
protected BukkitLogWatcher() {
|
|
||||||
super("ChromaDiscord",
|
|
||||||
LevelRangeFilter.createFilter(Level.INFO, Level.INFO, Filter.Result.ACCEPT, Filter.Result.DENY),
|
|
||||||
PatternLayout.createDefaultLayout());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void append(LogEvent logEvent) {
|
|
||||||
if (logEvent.getMessage().getFormattedMessage().contains("Attempting to restart with "))
|
|
||||||
MinecraftChatModule.state = DPState.RESTARTING_SERVER;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
public enum ChannelconBroadcast {
|
|
||||||
JOINLEAVE,
|
|
||||||
AFK,
|
|
||||||
RESTART,
|
|
||||||
DEATH,
|
|
||||||
BROADCAST;
|
|
||||||
|
|
||||||
public final int flag;
|
|
||||||
|
|
||||||
ChannelconBroadcast() {
|
|
||||||
this.flag = 1 << this.ordinal();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.bukkit.scheduler.BukkitScheduler;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
public class ChromaBot {
|
|
||||||
/**
|
|
||||||
* May be null if it's not initialized. Initialization happens after the server is done loading (using {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)})
|
|
||||||
*/
|
|
||||||
private static @Getter ChromaBot instance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will set the instance field.
|
|
||||||
*/
|
|
||||||
ChromaBot() {
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void delete() {
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to the chat channels and private chats.
|
|
||||||
*
|
|
||||||
* @param message The message to send, duh (use {@link MessageChannel#createMessage(String)})
|
|
||||||
*/
|
|
||||||
public void sendMessage(Function<Mono<MessageChannel>, Mono<Message>> message) {
|
|
||||||
MCChatUtils.forPublicPrivateChat(message::apply).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to the chat channels, private chats and custom chats.
|
|
||||||
*
|
|
||||||
* @param message The message to send, duh
|
|
||||||
* @param toggle The toggle type for channelcon
|
|
||||||
*/
|
|
||||||
public void sendMessageCustomAsWell(Function<Mono<MessageChannel>, Mono<Message>> message, @Nullable ChannelconBroadcast toggle) {
|
|
||||||
MCChatUtils.forCustomAndAllMCChat(message::apply, toggle, false).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updatePlayerList() {
|
|
||||||
MCChatUtils.updatePlayerList();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.IHaveConfig;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.Guild;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.core.spec.EmbedCreateSpec;
|
|
||||||
import lombok.val;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public final class DPUtils {
|
|
||||||
|
|
||||||
private static final Pattern URL_PATTERN = Pattern.compile("https?://\\S*");
|
|
||||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("[*_~]");
|
|
||||||
|
|
||||||
public static EmbedCreateSpec embedWithHead(EmbedCreateSpec ecs, String displayname, String playername, String profileUrl) {
|
|
||||||
return ecs.setAuthor(displayname, profileUrl, "https://minotar.net/avatar/" + playername + "/32.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes §[char] colour codes from strings & escapes them for Discord <br>
|
|
||||||
* Ensure that this method only gets called once (escaping)
|
|
||||||
*/
|
|
||||||
public static String sanitizeString(String string) {
|
|
||||||
return escape(sanitizeStringNoEscape(string));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes §[char] colour codes from strings
|
|
||||||
*/
|
|
||||||
public static String sanitizeStringNoEscape(String string) {
|
|
||||||
StringBuilder sanitizedString = new StringBuilder();
|
|
||||||
boolean random = false;
|
|
||||||
for (int i = 0; i < string.length(); i++) {
|
|
||||||
if (string.charAt(i) == '§') {
|
|
||||||
i++;// Skips the data value, the 4 in "§4Alisolarflare"
|
|
||||||
random = string.charAt(i) == 'k';
|
|
||||||
} else {
|
|
||||||
if (!random) // Skip random/obfuscated characters
|
|
||||||
sanitizedString.append(string.charAt(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sanitizedString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escape(String message) {
|
|
||||||
//var ts = new TreeSet<>();
|
|
||||||
var ts = new TreeSet<int[]>(Comparator.comparingInt(a -> a[0])); //Compare the start, then check the end
|
|
||||||
var matcher = URL_PATTERN.matcher(message);
|
|
||||||
while (matcher.find())
|
|
||||||
ts.add(new int[]{matcher.start(), matcher.end()});
|
|
||||||
matcher = FORMAT_PATTERN.matcher(message);
|
|
||||||
/*Function<MatchResult, String> aFunctionalInterface = result ->
|
|
||||||
Optional.ofNullable(ts.floor(new int[]{result.start(), 0})).map(a -> a[1]).orElse(0) < result.start()
|
|
||||||
? "\\\\" + result.group() : result.group();
|
|
||||||
return matcher.replaceAll(aFunctionalInterface); //Find nearest URL match and if it's not reaching to the char then escape*/
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
while (matcher.find()) {
|
|
||||||
matcher.appendReplacement(sb, Optional.ofNullable(ts.floor(new int[]{matcher.start(), 0})) //Find a URL start <= our start
|
|
||||||
.map(a -> a[1]).orElse(-1) < matcher.start() //Check if URL end < our start
|
|
||||||
? "\\\\" + matcher.group() : matcher.group());
|
|
||||||
}
|
|
||||||
matcher.appendTail(sb);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Logger getLogger() {
|
|
||||||
if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null)
|
|
||||||
return Logger.getLogger("DiscordPlugin");
|
|
||||||
return DiscordPlugin.plugin.getLogger();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key) {
|
|
||||||
return config.getReadOnlyDataPrimDef(key, 0L, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> 0L); //We can afford to search for the channel in the cache once (instead of using mainServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName) {
|
|
||||||
return roleData(config, key, defName, Mono.just(DiscordPlugin.mainServer));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Needs to be a {@link ConfigData} for checking if it's set
|
|
||||||
*/
|
|
||||||
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName, Mono<Guild> guild) {
|
|
||||||
return config.getReadOnlyDataPrimDef(key, defName, name -> {
|
|
||||||
if (!(name instanceof String) || ((String) name).length() == 0) return Mono.empty();
|
|
||||||
return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).onErrorResume(e -> {
|
|
||||||
getLogger().warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage());
|
|
||||||
return Mono.empty();
|
|
||||||
}).next();
|
|
||||||
}, r -> defName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ReadOnlyConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) {
|
|
||||||
return config.getReadOnlyDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mentions the <b>bot channel</b>. Useful for help texts.
|
|
||||||
*
|
|
||||||
* @return The string for mentioning the channel
|
|
||||||
*/
|
|
||||||
public static String botmention() {
|
|
||||||
if (DiscordPlugin.plugin == null) return "#bot";
|
|
||||||
return channelMention(DiscordPlugin.plugin.commandChannel.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the component if one of the given configs return null. Useful for channel/role configs.
|
|
||||||
*
|
|
||||||
* @param component The component to disable if needed
|
|
||||||
* @param configs The configs to check for null
|
|
||||||
* @return Whether the component got disabled and a warning logged
|
|
||||||
*/
|
|
||||||
public static boolean disableIfConfigError(@Nullable Component<DiscordPlugin> component, ConfigData<?>... configs) {
|
|
||||||
for (val config : configs) {
|
|
||||||
Object v = config.get();
|
|
||||||
if (disableIfConfigErrorRes(component, config, v))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the component if one of the given configs return null. Useful for channel/role configs.
|
|
||||||
*
|
|
||||||
* @param component The component to disable if needed
|
|
||||||
* @param config The (snowflake) config to check for null
|
|
||||||
* @param result The result of getting the value
|
|
||||||
* @return Whether the component got disabled and a warning logged
|
|
||||||
*/
|
|
||||||
public static boolean disableIfConfigErrorRes(@Nullable Component<DiscordPlugin> component, ConfigData<?> config, Object result) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (result == null || (result instanceof Mono<?> && !((Mono<?>) result).hasElement().block())) {
|
|
||||||
String path = null;
|
|
||||||
try {
|
|
||||||
if (component != null)
|
|
||||||
Component.setComponentEnabled(component, false);
|
|
||||||
path = config.getPath();
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (component != null)
|
|
||||||
TBMCCoreAPI.SendException("Failed to disable component after config error!", e, component);
|
|
||||||
else
|
|
||||||
TBMCCoreAPI.SendException("Failed to disable component after config error!", e, DiscordPlugin.plugin);
|
|
||||||
}
|
|
||||||
getLogger().warning("The config value " + path + " isn't set correctly " + (component == null ? "in global settings!" : "for component " + component.getClass().getSimpleName() + "!"));
|
|
||||||
getLogger().warning("Set the correct ID in the config" + (component == null ? "" : " or disable this component") + " to remove this message.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a response in the form of "@User, message". Use Mono.empty() if you don't have a channel object.
|
|
||||||
*
|
|
||||||
* @param original The original message to reply to
|
|
||||||
* @param channel The channel to send the message in, defaults to the original
|
|
||||||
* @param message The message to send
|
|
||||||
* @return A mono to send the message
|
|
||||||
*/
|
|
||||||
public static Mono<Message> reply(Message original, @Nullable MessageChannel channel, String message) {
|
|
||||||
Mono<MessageChannel> ch;
|
|
||||||
if (channel == null)
|
|
||||||
ch = original.getChannel();
|
|
||||||
else
|
|
||||||
ch = Mono.just(channel);
|
|
||||||
return reply(original, ch, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see #reply(Message, MessageChannel, String)
|
|
||||||
*/
|
|
||||||
public static Mono<Message> reply(Message original, Mono<MessageChannel> ch, String message) {
|
|
||||||
return ch.flatMap(chan -> chan.createMessage((original.getAuthor().isPresent()
|
|
||||||
? original.getAuthor().get().getMention() + ", " : "") + message));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String nickMention(Snowflake userId) {
|
|
||||||
return "<@!" + userId.asString() + ">";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String channelMention(Snowflake channelId) {
|
|
||||||
return "<#" + channelId.asString() + ">";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a message channel for a config. Returns empty for ID 0.
|
|
||||||
*
|
|
||||||
* @param key The config key
|
|
||||||
* @param id The channel ID
|
|
||||||
* @return A message channel
|
|
||||||
*/
|
|
||||||
public static Mono<MessageChannel> getMessageChannel(String key, Snowflake id) {
|
|
||||||
if (id.asLong() == 0L) return Mono.empty();
|
|
||||||
return DiscordPlugin.dc.getChannelById(id).onErrorResume(e -> {
|
|
||||||
getLogger().warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage());
|
|
||||||
return Mono.empty();
|
|
||||||
}).filter(ch -> ch instanceof MessageChannel).cast(MessageChannel.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Mono<MessageChannel> getMessageChannel(ConfigData<Snowflake> config) {
|
|
||||||
return getMessageChannel(config.getPath(), config.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> Mono<T> ignoreError(Mono<T> mono) {
|
|
||||||
return mono.onErrorResume(t -> Mono.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,325 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.DiscordInventory;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VCMDWrapper;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.Delegate;
|
|
||||||
import net.md_5.bungee.api.ChatMessageType;
|
|
||||||
import net.md_5.bungee.api.chat.BaseComponent;
|
|
||||||
import org.bukkit.*;
|
|
||||||
import org.bukkit.attribute.Attribute;
|
|
||||||
import org.bukkit.attribute.AttributeInstance;
|
|
||||||
import org.bukkit.attribute.AttributeModifier;
|
|
||||||
import org.bukkit.entity.Entity;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
|
||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
|
||||||
import org.bukkit.inventory.Inventory;
|
|
||||||
import org.bukkit.inventory.PlayerInventory;
|
|
||||||
import org.bukkit.permissions.PermissibleBase;
|
|
||||||
import org.bukkit.permissions.ServerOperator;
|
|
||||||
import org.mockito.MockSettings;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static org.mockito.Answers.RETURNS_DEFAULTS;
|
|
||||||
|
|
||||||
public abstract class DiscordConnectedPlayer extends DiscordSenderBase implements IMCPlayer<DiscordConnectedPlayer> {
|
|
||||||
private @Getter VCMDWrapper vanillaCmdListener;
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private boolean loggedIn = false;
|
|
||||||
|
|
||||||
@Delegate(excludes = ServerOperator.class)
|
|
||||||
private PermissibleBase origPerm;
|
|
||||||
|
|
||||||
private @Getter String name;
|
|
||||||
|
|
||||||
private @Getter OfflinePlayer basePlayer;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private PermissibleBase perm;
|
|
||||||
|
|
||||||
private Location location;
|
|
||||||
|
|
||||||
private final MinecraftChatModule module;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final UUID uniqueId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The parameters must match with {@link #create(User, MessageChannel, UUID, String, MinecraftChatModule)}
|
|
||||||
*/
|
|
||||||
protected DiscordConnectedPlayer(User user, MessageChannel channel, UUID uuid, String mcname,
|
|
||||||
MinecraftChatModule module) {
|
|
||||||
super(user, channel);
|
|
||||||
location = Bukkit.getWorlds().get(0).getSpawnLocation();
|
|
||||||
origPerm = perm = new PermissibleBase(basePlayer = Bukkit.getOfflinePlayer(uuid));
|
|
||||||
name = mcname;
|
|
||||||
this.module = module;
|
|
||||||
uniqueId = uuid;
|
|
||||||
displayName = mcname;
|
|
||||||
vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, module));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For testing
|
|
||||||
*/
|
|
||||||
protected DiscordConnectedPlayer(User user, MessageChannel channel) {
|
|
||||||
super(user, channel);
|
|
||||||
module = null;
|
|
||||||
uniqueId = UUID.randomUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOp(boolean value) { //CraftPlayer-compatible implementation
|
|
||||||
this.origPerm.setOp(value);
|
|
||||||
this.perm.recalculatePermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOp() { return this.origPerm.isOp(); }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean teleport(Location location) {
|
|
||||||
if (module.allowFakePlayerTeleports.get())
|
|
||||||
this.location = location;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
|
|
||||||
if (module.allowFakePlayerTeleports.get())
|
|
||||||
this.location = location;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean teleport(Entity destination) {
|
|
||||||
if (module.allowFakePlayerTeleports.get())
|
|
||||||
this.location = destination.getLocation();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean teleport(Entity destination, PlayerTeleportEvent.TeleportCause cause) {
|
|
||||||
if (module.allowFakePlayerTeleports.get())
|
|
||||||
this.location = destination.getLocation();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Location getLocation(Location loc) {
|
|
||||||
if (loc != null) {
|
|
||||||
loc.setWorld(getWorld());
|
|
||||||
loc.setX(location.getX());
|
|
||||||
loc.setY(location.getY());
|
|
||||||
loc.setZ(location.getZ());
|
|
||||||
loc.setYaw(location.getYaw());
|
|
||||||
loc.setPitch(location.getPitch());
|
|
||||||
}
|
|
||||||
|
|
||||||
return loc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Server getServer() {
|
|
||||||
return Bukkit.getServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendRawMessage(String message) {
|
|
||||||
sendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void chat(String msg) {
|
|
||||||
Bukkit.getPluginManager()
|
|
||||||
.callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public World getWorld() {
|
|
||||||
return Bukkit.getWorlds().get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOnline() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Location getLocation() {
|
|
||||||
return new Location(getWorld(), location.getX(), location.getY(), location.getZ(),
|
|
||||||
location.getYaw(), location.getPitch());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Location getEyeLocation() {
|
|
||||||
return getLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public double getMaxHealth() {
|
|
||||||
return 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Player getPlayer() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private String displayName;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AttributeInstance getAttribute(Attribute attribute) {
|
|
||||||
return new AttributeInstance() {
|
|
||||||
@Override
|
|
||||||
public Attribute getAttribute() {
|
|
||||||
return attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getBaseValue() {
|
|
||||||
return getDefaultValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBaseValue(double value) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<AttributeModifier> getModifiers() {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addModifier(AttributeModifier modifier) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeModifier(AttributeModifier modifier) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getValue() {
|
|
||||||
return getDefaultValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getDefaultValue() {
|
|
||||||
return 20; //Works for max health, should be okay for the rest
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GameMode getGameMode() {
|
|
||||||
return GameMode.SPECTATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private final Player.Spigot spigot = new Player.Spigot() {
|
|
||||||
@Override
|
|
||||||
public InetSocketAddress getRawAddress() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playEffect(Location location, Effect effect, int id, int data, float offsetX, float offsetY, float offsetZ, float speed, int particleCount, int radius) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getCollidesWithEntities() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCollidesWithEntities(boolean collides) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void respawn() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getLocale() {
|
|
||||||
return "en_us";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<Player> getHiddenPlayers() {
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(BaseComponent component) {
|
|
||||||
DiscordConnectedPlayer.super.sendMessage(component.toPlainText());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(BaseComponent... components) {
|
|
||||||
for (var component : components)
|
|
||||||
sendMessage(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(ChatMessageType position, BaseComponent component) {
|
|
||||||
sendMessage(component); //Ignore position
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(ChatMessageType position, BaseComponent... components) {
|
|
||||||
sendMessage(components); //Ignore position
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInvulnerable() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Player.Spigot spigot() {
|
|
||||||
return spigot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DiscordConnectedPlayer create(User user, MessageChannel channel, UUID uuid, String mcname,
|
|
||||||
MinecraftChatModule module) {
|
|
||||||
return Mockito.mock(DiscordConnectedPlayer.class,
|
|
||||||
getSettings().useConstructor(user, channel, uuid, mcname, module));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DiscordConnectedPlayer createTest() {
|
|
||||||
return Mockito.mock(DiscordConnectedPlayer.class, getSettings().useConstructor(null, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MockSettings getSettings() {
|
|
||||||
return Mockito.withSettings()
|
|
||||||
.defaultAnswer(invocation -> {
|
|
||||||
try {
|
|
||||||
if (!Modifier.isAbstract(invocation.getMethod().getModifiers()))
|
|
||||||
return invocation.callRealMethod();
|
|
||||||
if (PlayerInventory.class.isAssignableFrom(invocation.getMethod().getReturnType()))
|
|
||||||
return Mockito.mock(DiscordInventory.class, Mockito.withSettings().extraInterfaces(PlayerInventory.class));
|
|
||||||
if (Inventory.class.isAssignableFrom(invocation.getMethod().getReturnType()))
|
|
||||||
return new DiscordInventory();
|
|
||||||
return RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Error in mocked player!");
|
|
||||||
e.printStackTrace();
|
|
||||||
return RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.stubOnly();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatPrivate;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
|
||||||
import buttondevteam.lib.player.UserClass;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
|
|
||||||
@UserClass(foldername = "discord")
|
|
||||||
public class DiscordPlayer extends ChromaGamerBase {
|
|
||||||
private String did;
|
|
||||||
// private @Getter @Setter boolean minecraftChatEnabled;
|
|
||||||
|
|
||||||
public DiscordPlayer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDiscordID() {
|
|
||||||
if (did == null)
|
|
||||||
did = getFileName();
|
|
||||||
return did;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if player has the private Minecraft chat enabled. For setting the value, see
|
|
||||||
* {@link MCChatPrivate#privateMCChat(MessageChannel, boolean, User, DiscordPlayer)}
|
|
||||||
*/
|
|
||||||
public boolean isMinecraftChatEnabled() {
|
|
||||||
return MCChatPrivate.isMinecraftChatEnabled(this);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VCMDWrapper;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
|
|
||||||
public abstract class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer<DiscordPlayerSender> {
|
|
||||||
|
|
||||||
protected Player player;
|
|
||||||
private @Getter final VCMDWrapper vanillaCmdListener;
|
|
||||||
|
|
||||||
public DiscordPlayerSender(User user, MessageChannel channel, Player player, MinecraftChatModule module) {
|
|
||||||
super(user, channel);
|
|
||||||
this.player = player;
|
|
||||||
vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player, module));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String message) {
|
|
||||||
player.sendMessage(message);
|
|
||||||
super.sendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String[] messages) {
|
|
||||||
player.sendMessage(messages);
|
|
||||||
super.sendMessage(messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DiscordPlayerSender create(User user, MessageChannel channel, Player player, MinecraftChatModule module) {
|
|
||||||
return Mockito.mock(DiscordPlayerSender.class, Mockito.withSettings().stubOnly().defaultAnswer(invocation -> {
|
|
||||||
if (!Modifier.isAbstract(invocation.getMethod().getModifiers()))
|
|
||||||
return invocation.callRealMethod();
|
|
||||||
return invocation.getMethod().invoke(((DiscordPlayerSender) invocation.getMock()).player, invocation.getArguments());
|
|
||||||
}).useConstructor(user, channel, player, module));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,292 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.announcer.AnnouncerModule;
|
|
||||||
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
|
|
||||||
import buttondevteam.discordplugin.commands.*;
|
|
||||||
import buttondevteam.discordplugin.exceptions.ExceptionListenerModule;
|
|
||||||
import buttondevteam.discordplugin.fun.FunModule;
|
|
||||||
import buttondevteam.discordplugin.listeners.CommonListeners;
|
|
||||||
import buttondevteam.discordplugin.listeners.MCListener;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.mccommands.DiscordMCCommand;
|
|
||||||
import buttondevteam.discordplugin.role.GameRoleModule;
|
|
||||||
import buttondevteam.discordplugin.util.DPState;
|
|
||||||
import buttondevteam.discordplugin.util.Timings;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.ButtonPlugin;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.IHaveConfig;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
|
||||||
import com.google.common.io.Files;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.DiscordClientBuilder;
|
|
||||||
import discord4j.core.GatewayDiscordClient;
|
|
||||||
import discord4j.core.event.domain.guild.GuildCreateEvent;
|
|
||||||
import discord4j.core.event.domain.lifecycle.ReadyEvent;
|
|
||||||
import discord4j.core.object.entity.Guild;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.presence.Activity;
|
|
||||||
import discord4j.core.object.presence.Presence;
|
|
||||||
import discord4j.core.object.reaction.ReactionEmoji;
|
|
||||||
import discord4j.store.jdk.JdkStoreService;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.core.Logger;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.mockito.internal.util.MockUtil;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@ButtonPlugin.ConfigOpts(disableConfigGen = true)
|
|
||||||
public class DiscordPlugin extends ButtonPlugin {
|
|
||||||
public static GatewayDiscordClient dc;
|
|
||||||
public static DiscordPlugin plugin;
|
|
||||||
public static boolean SafeMode = true;
|
|
||||||
@Getter
|
|
||||||
private Command2DC manager;
|
|
||||||
private boolean starting;
|
|
||||||
private BukkitLogWatcher logWatcher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The prefix to use with Discord commands like /role. It only works in the bot channel.
|
|
||||||
*/
|
|
||||||
private final ConfigData<Character> prefix = getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString);
|
|
||||||
|
|
||||||
public static char getPrefix() {
|
|
||||||
if (plugin == null) return '/';
|
|
||||||
return plugin.prefix.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
|
|
||||||
*/
|
|
||||||
private ConfigData<Optional<Guild>> mainServer() {
|
|
||||||
return getIConfig().getDataPrimDef("mainServer", 0L,
|
|
||||||
id -> {
|
|
||||||
//It attempts to get the default as well
|
|
||||||
if ((long) id == 0L)
|
|
||||||
return Optional.empty(); //Hack?
|
|
||||||
return dc.getGuildById(Snowflake.of((long) id))
|
|
||||||
.onErrorResume(t -> Mono.fromRunnable(() -> getLogger().warning("Failed to get guild: " + t.getMessage()))).blockOptional();
|
|
||||||
},
|
|
||||||
g -> g.map(gg -> gg.getId().asLong()).orElse(0L));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The (bot) channel to use for Discord commands like /role.
|
|
||||||
*/
|
|
||||||
public ConfigData<Snowflake> commandChannel = DPUtils.snowflakeData(getIConfig(), "commandChannel", 0L);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The role that allows using mod-only Discord commands.
|
|
||||||
* If empty (''), then it will only allow for the owner.
|
|
||||||
*/
|
|
||||||
public ConfigData<Mono<Role>> modRole;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
|
|
||||||
*/
|
|
||||||
public ConfigData<String> inviteLink = getIConfig().getData("inviteLink", "");
|
|
||||||
|
|
||||||
private void setupConfig() {
|
|
||||||
modRole = DPUtils.roleData(getIConfig(), "modRole", "Moderator");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoad() { //Needed by ServerWatcher
|
|
||||||
var thread = Thread.currentThread();
|
|
||||||
var cl = thread.getContextClassLoader();
|
|
||||||
thread.setContextClassLoader(getClassLoader());
|
|
||||||
MockUtil.isMock(null); //Load MockUtil to load Mockito plugins
|
|
||||||
thread.setContextClassLoader(cl);
|
|
||||||
getLogger().info("Load complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pluginEnable() {
|
|
||||||
try {
|
|
||||||
getLogger().info("Initializing...");
|
|
||||||
plugin = this;
|
|
||||||
manager = new Command2DC();
|
|
||||||
registerCommand(new DiscordMCCommand()); //Register so that the restart command works
|
|
||||||
String token;
|
|
||||||
File tokenFile = new File("TBMC", "Token.txt");
|
|
||||||
if (tokenFile.exists()) //Legacy support
|
|
||||||
//noinspection UnstableApiUsage
|
|
||||||
token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8);
|
|
||||||
else {
|
|
||||||
File privateFile = new File(getDataFolder(), "private.yml");
|
|
||||||
val conf = YamlConfiguration.loadConfiguration(privateFile);
|
|
||||||
token = conf.getString("token");
|
|
||||||
if (token == null || token.equalsIgnoreCase("Token goes here")) {
|
|
||||||
conf.set("token", "Token goes here");
|
|
||||||
conf.save(privateFile);
|
|
||||||
|
|
||||||
getLogger().severe("Token not found! Please set it in private.yml then do /discord restart");
|
|
||||||
getLogger().severe("You need to have a bot account to use with your server.");
|
|
||||||
getLogger().severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
starting = true;
|
|
||||||
//System.out.println("This line should show up for sure");
|
|
||||||
val cb = DiscordClientBuilder.create(token).build().gateway();
|
|
||||||
//System.out.println("Got gateway bootstrap");
|
|
||||||
cb.setInitialStatus(si -> Presence.doNotDisturb(Activity.playing("booting")));
|
|
||||||
cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now
|
|
||||||
//System.out.println("Initial status and store service set");
|
|
||||||
cb.login().doOnError(t -> {
|
|
||||||
stopStarting();
|
|
||||||
//System.out.println("Got this error: " + t); t.printStackTrace();
|
|
||||||
}).subscribe(dc -> {
|
|
||||||
//System.out.println("Login successful, got dc: " + dc);
|
|
||||||
DiscordPlugin.dc = dc; //Set to gateway client
|
|
||||||
dc.on(ReadyEvent.class) // Listen for ReadyEvent(s)
|
|
||||||
.map(event -> event.getGuilds().size()) // Get how many guilds the bot is in
|
|
||||||
.flatMap(size -> dc
|
|
||||||
.on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s)
|
|
||||||
.take(size) // Take only the first `size` GuildCreateEvent(s) to be received
|
|
||||||
.doOnError(t -> {
|
|
||||||
stopStarting();
|
|
||||||
//System.out.println("Got error: " + t);
|
|
||||||
t.printStackTrace();
|
|
||||||
})
|
|
||||||
.collectList()).doOnError(t -> stopStarting()).subscribe(this::handleReady); // Take all received GuildCreateEvents and make it a List
|
|
||||||
}); /* All guilds have been received, client is fully connected */
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this);
|
|
||||||
getLogger().severe("You may be able to restart the plugin using /discord restart");
|
|
||||||
stopStarting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopStarting() {
|
|
||||||
synchronized (this) {
|
|
||||||
starting = false;
|
|
||||||
notifyAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Guild mainServer;
|
|
||||||
|
|
||||||
private void handleReady(List<GuildCreateEvent> event) {
|
|
||||||
//System.out.println("Got ready event");
|
|
||||||
try {
|
|
||||||
if (mainServer != null) { //This is not the first ready event
|
|
||||||
getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections
|
|
||||||
dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); //Update from the initial presence
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards
|
|
||||||
if (mainServer == null) {
|
|
||||||
if (event.size() == 0) {
|
|
||||||
getLogger().severe("Main server not found! Invite the bot and do /discord restart");
|
|
||||||
dc.getApplicationInfo().subscribe(info ->
|
|
||||||
getLogger().severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId().asString() + "&scope=bot&permissions=268509264"));
|
|
||||||
saveConfig(); //Put default there
|
|
||||||
return; //We should have all guilds by now, no need to retry
|
|
||||||
}
|
|
||||||
mainServer = event.get(0).getGuild();
|
|
||||||
getLogger().warning("Main server set to first one: " + mainServer.getName());
|
|
||||||
mainServer().set(Optional.of(mainServer)); //Save in config
|
|
||||||
}
|
|
||||||
SafeMode = false;
|
|
||||||
setupConfig();
|
|
||||||
DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel));
|
|
||||||
//Won't disable, just prints the warning here
|
|
||||||
|
|
||||||
if (MinecraftChatModule.state == DPState.STOPPING_SERVER) {
|
|
||||||
stopStarting();
|
|
||||||
return; //Reusing that field to check if stopping while still initializing
|
|
||||||
}
|
|
||||||
CommonListeners.register(dc.getEventDispatcher());
|
|
||||||
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this);
|
|
||||||
TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class, DiscordPlayer::new);
|
|
||||||
ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase
|
|
||||||
? ((DiscordSenderBase) sender).getChromaUser() : null));
|
|
||||||
|
|
||||||
IHaveConfig.pregenConfig(this, null);
|
|
||||||
|
|
||||||
var cb = new ChromaBot(); //Initialize ChromaBot
|
|
||||||
Component.registerComponent(this, new GeneralEventBroadcasterModule());
|
|
||||||
Component.registerComponent(this, new MinecraftChatModule());
|
|
||||||
Component.registerComponent(this, new ExceptionListenerModule());
|
|
||||||
Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set
|
|
||||||
Component.registerComponent(this, new AnnouncerModule());
|
|
||||||
Component.registerComponent(this, new FunModule());
|
|
||||||
cb.updatePlayerList(); //The MCChatModule is tested to be enabled
|
|
||||||
|
|
||||||
getManager().registerCommand(new VersionCommand());
|
|
||||||
getManager().registerCommand(new UserinfoCommand());
|
|
||||||
getManager().registerCommand(new HelpCommand());
|
|
||||||
getManager().registerCommand(new DebugCommand());
|
|
||||||
getManager().registerCommand(new ConnectCommand());
|
|
||||||
|
|
||||||
TBMCCoreAPI.SendUnsentExceptions();
|
|
||||||
TBMCCoreAPI.SendUnsentDebugMessages();
|
|
||||||
|
|
||||||
var blw = new BukkitLogWatcher();
|
|
||||||
blw.start();
|
|
||||||
((Logger) LogManager.getRootLogger()).addAppender(blw);
|
|
||||||
logWatcher = blw;
|
|
||||||
|
|
||||||
if (!TBMCCoreAPI.IsTestServer()) {
|
|
||||||
dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe();
|
|
||||||
} else {
|
|
||||||
dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe();
|
|
||||||
}
|
|
||||||
getLogger().info("Loaded!");
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this);
|
|
||||||
}
|
|
||||||
stopStarting();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pluginPreDisable() {
|
|
||||||
if (MinecraftChatModule.state == DPState.RUNNING)
|
|
||||||
MinecraftChatModule.state = DPState.STOPPING_SERVER;
|
|
||||||
synchronized (this) {
|
|
||||||
if (starting) {
|
|
||||||
try {
|
|
||||||
wait(10000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ChromaBot.getInstance() == null) return; //Failed to load
|
|
||||||
Timings timings = new Timings();
|
|
||||||
timings.printElapsed("Disable start");
|
|
||||||
timings.printElapsed("Updating player list");
|
|
||||||
ChromaBot.getInstance().updatePlayerList();
|
|
||||||
timings.printElapsed("Done");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pluginDisable() {
|
|
||||||
Timings timings = new Timings();
|
|
||||||
timings.printElapsed("Actual disable start (logout)");
|
|
||||||
if (ChromaBot.getInstance() == null) return; //Failed to load
|
|
||||||
|
|
||||||
try {
|
|
||||||
SafeMode = true; // Stop interacting with Discord
|
|
||||||
ChromaBot.delete();
|
|
||||||
((Logger) LogManager.getRootLogger()).removeAppender(logWatcher);
|
|
||||||
timings.printElapsed("Logging out...");
|
|
||||||
dc.logout().block();
|
|
||||||
mainServer = null; //Allow ReadyEvent again
|
|
||||||
//Configs are emptied so channels and servers are fetched again
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("✅");
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import discord4j.core.object.entity.Member;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Server;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.permissions.PermissibleBase;
|
|
||||||
import org.bukkit.permissions.Permission;
|
|
||||||
import org.bukkit.permissions.PermissionAttachment;
|
|
||||||
import org.bukkit.permissions.PermissionAttachmentInfo;
|
|
||||||
import org.bukkit.plugin.Plugin;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class DiscordSender extends DiscordSenderBase implements CommandSender {
|
|
||||||
private PermissibleBase perm = new PermissibleBase(this);
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
public DiscordSender(User user, MessageChannel channel) {
|
|
||||||
super(user, channel);
|
|
||||||
val def = "Discord user";
|
|
||||||
name = user == null ? def : user.asMember(DiscordPlugin.mainServer.getId())
|
|
||||||
.onErrorResume(t -> Mono.empty()).blockOptional().map(Member::getDisplayName).orElse(def);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DiscordSender(User user, MessageChannel channel, String name) {
|
|
||||||
super(user, channel);
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPermissionSet(String name) {
|
|
||||||
return perm.isPermissionSet(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPermissionSet(Permission perm) {
|
|
||||||
return this.perm.isPermissionSet(perm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(String name) {
|
|
||||||
if (name.contains("essentials") && !name.equals("essentials.list"))
|
|
||||||
return false;
|
|
||||||
return perm.hasPermission(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(Permission perm) {
|
|
||||||
return this.perm.hasPermission(perm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
|
|
||||||
return perm.addAttachment(plugin, name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PermissionAttachment addAttachment(Plugin plugin) {
|
|
||||||
return perm.addAttachment(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
|
|
||||||
return perm.addAttachment(plugin, name, value, ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
|
|
||||||
return perm.addAttachment(plugin, ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAttachment(PermissionAttachment attachment) {
|
|
||||||
perm.removeAttachment(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void recalculatePermissions() {
|
|
||||||
perm.recalculatePermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<PermissionAttachmentInfo> getEffectivePermissions() {
|
|
||||||
return perm.getEffectivePermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOp() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOp(boolean value) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Server getServer() {
|
|
||||||
return Bukkit.getServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Spigot spigot() {
|
|
||||||
return new CommandSender.Spigot();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
|
||||||
|
|
||||||
public abstract class DiscordSenderBase implements CommandSender {
|
|
||||||
/**
|
|
||||||
* May be null.
|
|
||||||
*/
|
|
||||||
protected User user;
|
|
||||||
protected MessageChannel channel;
|
|
||||||
|
|
||||||
protected DiscordSenderBase(User user, MessageChannel channel) {
|
|
||||||
this.user = user;
|
|
||||||
this.channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private volatile String msgtosend = "";
|
|
||||||
private volatile BukkitTask sendtask;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user. May be null.
|
|
||||||
*
|
|
||||||
* @return The user or null.
|
|
||||||
*/
|
|
||||||
public User getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageChannel getChannel() {
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DiscordPlayer chromaUser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the user data on first query.
|
|
||||||
*
|
|
||||||
* @return A Chroma user of Discord or a Discord user of Chroma
|
|
||||||
*/
|
|
||||||
public DiscordPlayer getChromaUser() {
|
|
||||||
if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getId().asString(), DiscordPlayer.class);
|
|
||||||
return chromaUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String message) {
|
|
||||||
try {
|
|
||||||
final boolean broadcast = new Exception().getStackTrace()[2].getMethodName().contains("broadcast");
|
|
||||||
if (broadcast) //We're catching broadcasts using the Bukkit event
|
|
||||||
return;
|
|
||||||
final String sendmsg = DPUtils.sanitizeString(message);
|
|
||||||
synchronized (this) {
|
|
||||||
msgtosend += "\n" + sendmsg;
|
|
||||||
if (sendtask == null)
|
|
||||||
sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
|
|
||||||
channel.createMessage((user != null ? user.getMention() + "\n" : "") + msgtosend.trim()).subscribe();
|
|
||||||
sendtask = null;
|
|
||||||
msgtosend = "";
|
|
||||||
}, 4); // Waits a 0.2 second to gather all/most of the different messages
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e, DiscordPlugin.plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String[] messages) {
|
|
||||||
sendMessage(String.join("\n", messages));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package buttondevteam.discordplugin;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VCMDWrapper;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
public interface IMCPlayer<T> extends Player {
|
|
||||||
VCMDWrapper getVanillaCmdListener();
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
package buttondevteam.discordplugin.announcer;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ComponentMetadata;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.val;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts).
|
|
||||||
*/
|
|
||||||
@ComponentMetadata(enabledByDefault = false)
|
|
||||||
public class AnnouncerModule extends Component<DiscordPlugin> {
|
|
||||||
/**
|
|
||||||
* Channel to post new posts.
|
|
||||||
*/
|
|
||||||
public final ReadOnlyConfigData<Mono<MessageChannel>> channel = DPUtils.channelData(getConfig(), "channel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel where distinguished (moderator) posts go.
|
|
||||||
*/
|
|
||||||
private final ReadOnlyConfigData<Mono<MessageChannel>> modChannel = DPUtils.channelData(getConfig(), "modChannel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically unpins all messages except the last few. Set to 0 or >50 to disable
|
|
||||||
*/
|
|
||||||
private final ConfigData<Short> keepPinned = getConfig().getData("keepPinned", (short) 40);
|
|
||||||
|
|
||||||
private final ConfigData<Long> lastAnnouncementTime = getConfig().getData("lastAnnouncementTime", 0L);
|
|
||||||
|
|
||||||
private final ConfigData<Long> lastSeenTime = getConfig().getData("lastSeenTime", 0L);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The subreddit to pull the posts from
|
|
||||||
*/
|
|
||||||
private final ConfigData<String> subredditURL = getConfig().getData("subredditURL", "https://www.reddit.com/r/ChromaGamers");
|
|
||||||
|
|
||||||
private static boolean stop = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
if (DPUtils.disableIfConfigError(this, channel, modChannel)) return;
|
|
||||||
stop = false; //If not the first time
|
|
||||||
val kp = keepPinned.get();
|
|
||||||
if (kp == 0) return;
|
|
||||||
Flux<Message> msgs = channel.get().flatMapMany(MessageChannel::getPinnedMessages).takeLast(kp);
|
|
||||||
msgs.subscribe(Message::unpin);
|
|
||||||
new Thread(this::AnnouncementGetterThreadMethod).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
stop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AnnouncementGetterThreadMethod() {
|
|
||||||
while (!stop) {
|
|
||||||
try {
|
|
||||||
if (!isEnabled()) {
|
|
||||||
//noinspection BusyWait
|
|
||||||
Thread.sleep(10000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String body = TBMCCoreAPI.DownloadString(subredditURL.get() + "/new/.json?limit=10");
|
|
||||||
JsonArray json = new JsonParser().parse(body).getAsJsonObject().get("data").getAsJsonObject()
|
|
||||||
.get("children").getAsJsonArray();
|
|
||||||
StringBuilder msgsb = new StringBuilder();
|
|
||||||
StringBuilder modmsgsb = new StringBuilder();
|
|
||||||
long lastanntime = lastAnnouncementTime.get();
|
|
||||||
for (int i = json.size() - 1; i >= 0; i--) {
|
|
||||||
JsonObject item = json.get(i).getAsJsonObject();
|
|
||||||
final JsonObject data = item.get("data").getAsJsonObject();
|
|
||||||
String author = data.get("author").getAsString();
|
|
||||||
JsonElement distinguishedjson = data.get("distinguished");
|
|
||||||
String distinguished;
|
|
||||||
if (distinguishedjson.isJsonNull())
|
|
||||||
distinguished = null;
|
|
||||||
else
|
|
||||||
distinguished = distinguishedjson.getAsString();
|
|
||||||
String permalink = "https://www.reddit.com" + data.get("permalink").getAsString();
|
|
||||||
long date = data.get("created_utc").getAsLong();
|
|
||||||
if (date > lastSeenTime.get())
|
|
||||||
lastSeenTime.set(date);
|
|
||||||
else if (date > lastAnnouncementTime.get()) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
do {
|
|
||||||
val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit");
|
|
||||||
if (reddituserclass == null)
|
|
||||||
break;
|
|
||||||
val user = ChromaGamerBase.getUser(author, reddituserclass);
|
|
||||||
String id = user.getConnectedID(DiscordPlayer.class);
|
|
||||||
if (id != null)
|
|
||||||
author = "<@" + id + ">";
|
|
||||||
} while (false);
|
|
||||||
if (!author.startsWith("<"))
|
|
||||||
author = "/u/" + author;
|
|
||||||
(distinguished != null && distinguished.equals("moderator") ? modmsgsb : msgsb)
|
|
||||||
.append("A new post was submitted to the subreddit by ").append(author).append("\n")
|
|
||||||
.append(permalink).append("\n");
|
|
||||||
lastanntime = date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (msgsb.length() > 0)
|
|
||||||
channel.get().flatMap(ch -> ch.createMessage(msgsb.toString()))
|
|
||||||
.flatMap(Message::pin).subscribe();
|
|
||||||
if (modmsgsb.length() > 0)
|
|
||||||
modChannel.get().flatMap(ch -> ch.createMessage(modmsgsb.toString()))
|
|
||||||
.flatMap(Message::pin).subscribe();
|
|
||||||
if (lastAnnouncementTime.get() != lastanntime)
|
|
||||||
lastAnnouncementTime.set(lastanntime); // If sending succeeded
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
//noinspection BusyWait
|
|
||||||
Thread.sleep(10000);
|
|
||||||
} catch (InterruptedException ex) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package buttondevteam.discordplugin.broadcaster;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ComponentMetadata;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone.
|
|
||||||
* If this component is enabled then these messages will show up on Discord.
|
|
||||||
*/
|
|
||||||
@ComponentMetadata(enabledByDefault = false)
|
|
||||||
public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> {
|
|
||||||
private static @Getter boolean hooked = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
try {
|
|
||||||
PlayerListWatcher.hookUpDown(true, this);
|
|
||||||
log("Finished hooking into the player list");
|
|
||||||
hooked = true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Error while hacking the player list! Disable this module if you're on an incompatible version.", e, this);
|
|
||||||
} catch (NoClassDefFoundError e) {
|
|
||||||
logWarn("Error while hacking the player list! Disable this module if you're on an incompatible version.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
try {
|
|
||||||
if (!hooked) return;
|
|
||||||
if (PlayerListWatcher.hookUpDown(false, this))
|
|
||||||
log("Finished unhooking the player list!");
|
|
||||||
else
|
|
||||||
log("Didn't have the player list hooked.");
|
|
||||||
hooked = false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Error while hacking the player list!", e, this);
|
|
||||||
} catch (NoClassDefFoundError ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
package buttondevteam.discordplugin.broadcaster;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.DelegatingMockMaker;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker;
|
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class PlayerListWatcher {
|
|
||||||
private static Object plist;
|
|
||||||
private static Object mock;
|
|
||||||
private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16
|
|
||||||
|
|
||||||
static boolean hookUpDown(boolean up, GeneralEventBroadcasterModule module) throws Exception {
|
|
||||||
val csc = Bukkit.getServer().getClass();
|
|
||||||
Field conf = csc.getDeclaredField("console");
|
|
||||||
conf.setAccessible(true);
|
|
||||||
val server = conf.get(Bukkit.getServer());
|
|
||||||
val nms = server.getClass().getPackage().getName();
|
|
||||||
val dplc = Class.forName(nms + ".DedicatedPlayerList");
|
|
||||||
val currentPL = server.getClass().getMethod("getPlayerList").invoke(server);
|
|
||||||
if (up) {
|
|
||||||
if (currentPL == mock) {
|
|
||||||
module.logWarn("Player list already mocked!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
DelegatingMockMaker.getInstance().setMockMaker(new SubclassByteBuddyMockMaker());
|
|
||||||
val icbcl = Class.forName(nms + ".IChatBaseComponent");
|
|
||||||
Method sendMessageTemp;
|
|
||||||
try {
|
|
||||||
sendMessageTemp = server.getClass().getMethod("sendMessage", icbcl, UUID.class);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
sendMessageTemp = server.getClass().getMethod("sendMessage", icbcl);
|
|
||||||
}
|
|
||||||
val sendMessage = sendMessageTemp;
|
|
||||||
val cmtcl = Class.forName(nms + ".ChatMessageType");
|
|
||||||
val systemType = cmtcl.getDeclaredField("SYSTEM").get(null);
|
|
||||||
val chatType = cmtcl.getDeclaredField("CHAT").get(null);
|
|
||||||
|
|
||||||
val obc = csc.getPackage().getName();
|
|
||||||
val ccmcl = Class.forName(obc + ".util.CraftChatMessage");
|
|
||||||
val fixComponent = ccmcl.getMethod("fixComponent", icbcl);
|
|
||||||
val ppoc = Class.forName(nms + ".PacketPlayOutChat");
|
|
||||||
Constructor<?> ppocCTemp;
|
|
||||||
try {
|
|
||||||
ppocCTemp = ppoc.getConstructor(icbcl, cmtcl, UUID.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
ppocCTemp = ppoc.getConstructor(icbcl, cmtcl);
|
|
||||||
}
|
|
||||||
val ppocC = ppocCTemp;
|
|
||||||
val sendAll = dplc.getMethod("sendAll", Class.forName(nms + ".Packet"));
|
|
||||||
Method tpt;
|
|
||||||
try {
|
|
||||||
tpt = icbcl.getMethod("toPlainText");
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
tpt = icbcl.getMethod("getString");
|
|
||||||
}
|
|
||||||
val toPlainText = tpt;
|
|
||||||
val sysb = Class.forName(nms + ".SystemUtils").getField("b");
|
|
||||||
|
|
||||||
//Find the original method without overrides
|
|
||||||
Constructor<MethodHandles.Lookup> lookupConstructor;
|
|
||||||
if (nms.contains("1_16")) {
|
|
||||||
lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class);
|
|
||||||
lookupConstructor.setAccessible(true); //Create lookup with a given class instead of caller
|
|
||||||
} else lookupConstructor = null;
|
|
||||||
mock = Mockito.mock(dplc, Mockito.withSettings().defaultAnswer(new Answer<>() { // Cannot call super constructor
|
|
||||||
@Override
|
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
final Method method = invocation.getMethod();
|
|
||||||
if (!method.getName().equals("sendMessage")) {
|
|
||||||
if (method.getName().equals("sendAll")) {
|
|
||||||
sendAll(invocation.getArgument(0));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//In 1.16 it passes a reference to the player list to advancement data for each player
|
|
||||||
if (nms.contains("1_16") && method.getName().equals("f") && method.getParameterCount() > 0 && method.getParameterTypes()[0].getSimpleName().equals("EntityPlayer")) {
|
|
||||||
method.setAccessible(true);
|
|
||||||
if (fHandle == null) {
|
|
||||||
assert lookupConstructor != null;
|
|
||||||
var lookup = lookupConstructor.newInstance(mock.getClass());
|
|
||||||
fHandle = lookup.unreflectSpecial(method, mock.getClass()); //Special: super.method()
|
|
||||||
}
|
|
||||||
return fHandle.invoke(mock, invocation.getArgument(0)); //Invoke with our instance, so it passes that to advancement data, we have the fields as well
|
|
||||||
}
|
|
||||||
return method.invoke(plist, invocation.getArguments());
|
|
||||||
}
|
|
||||||
val args = invocation.getArguments();
|
|
||||||
val params = method.getParameterTypes();
|
|
||||||
if (params.length == 0) {
|
|
||||||
TBMCCoreAPI.SendException("Found a strange method",
|
|
||||||
new Exception("Found a sendMessage() method without arguments."), module);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (params[0].getSimpleName().equals("IChatBaseComponent[]"))
|
|
||||||
for (val arg : (Object[]) args[0])
|
|
||||||
sendMessage(arg, true);
|
|
||||||
else if (params[0].getSimpleName().equals("IChatBaseComponent"))
|
|
||||||
if (params.length > 1 && params[1].getSimpleName().equalsIgnoreCase("boolean"))
|
|
||||||
sendMessage(args[0], (Boolean) args[1]);
|
|
||||||
else
|
|
||||||
sendMessage(args[0], true);
|
|
||||||
else
|
|
||||||
TBMCCoreAPI.SendException("Found a method with interesting params",
|
|
||||||
new Exception("Found a sendMessage(" + params[0].getSimpleName() + ") method"), module);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessage(Object chatComponent, boolean system) {
|
|
||||||
try { //Converted to use reflection
|
|
||||||
if (sendMessage.getParameterCount() == 2)
|
|
||||||
sendMessage.invoke(server, chatComponent, sysb.get(null));
|
|
||||||
else
|
|
||||||
sendMessage.invoke(server, chatComponent);
|
|
||||||
Object chatmessagetype = system ? systemType : chatType;
|
|
||||||
|
|
||||||
// CraftBukkit start - we run this through our processor first so we can get web links etc
|
|
||||||
var comp = fixComponent.invoke(null, chatComponent);
|
|
||||||
var packet = ppocC.getParameterCount() == 3
|
|
||||||
? ppocC.newInstance(comp, chatmessagetype, sysb.get(null))
|
|
||||||
: ppocC.newInstance(comp, chatmessagetype);
|
|
||||||
this.sendAll(packet);
|
|
||||||
// CraftBukkit end
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occurred while passing a vanilla message through the player list", e, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendAll(Object packet) {
|
|
||||||
try { // Some messages get sent by directly constructing a packet
|
|
||||||
sendAll.invoke(plist, packet);
|
|
||||||
if (packet.getClass() == ppoc) {
|
|
||||||
Field msgf = ppoc.getDeclaredField("a");
|
|
||||||
msgf.setAccessible(true);
|
|
||||||
MCChatUtils.forPublicPrivateChat(MCChatUtils.send((String) toPlainText.invoke(msgf.get(packet)))).subscribe();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).stubOnly());
|
|
||||||
plist = currentPL;
|
|
||||||
for (var plc = dplc; plc != null; plc = plc.getSuperclass()) { //Set all fields
|
|
||||||
for (var f : plc.getDeclaredFields()) {
|
|
||||||
f.setAccessible(true);
|
|
||||||
Field modf = f.getClass().getDeclaredField("modifiers");
|
|
||||||
modf.setAccessible(true);
|
|
||||||
modf.set(f, f.getModifiers() & ~Modifier.FINAL);
|
|
||||||
f.set(mock, f.get(plist));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
server.getClass().getMethod("a", dplc).invoke(server, up ? mock : plist);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
server.getClass().getMethod("a", Class.forName(server.getClass().getPackage().getName() + ".PlayerList"))
|
|
||||||
.invoke(server, up ? mock : plist);
|
|
||||||
}
|
|
||||||
Field pllf = csc.getDeclaredField("playerList");
|
|
||||||
pllf.setAccessible(true);
|
|
||||||
pllf.set(Bukkit.getServer(), up ? mock : plist);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class Command2DC extends Command2<ICommand2DC, Command2DCSender> {
|
|
||||||
@Override
|
|
||||||
public void registerCommand(ICommand2DC command) {
|
|
||||||
super.registerCommand(command, DiscordPlugin.getPrefix()); //Needs to be configurable for the helps
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(Command2DCSender sender, ICommand2DC command, Method method) {
|
|
||||||
//return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way?
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.lib.chat.Command2Sender;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class Command2DCSender implements Command2Sender {
|
|
||||||
private final @Getter
|
|
||||||
Message message;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String message) {
|
|
||||||
if (message.length() == 0) return;
|
|
||||||
message = DPUtils.sanitizeString(message);
|
|
||||||
message = Character.toLowerCase(message.charAt(0)) + message.substring(1);
|
|
||||||
val msg = message;
|
|
||||||
/*this.message.getAuthorAsMember().flatMap(author ->
|
|
||||||
this.message.getChannel().flatMap(ch ->
|
|
||||||
ch.createMessage(author.getNicknameMention() + ", " + msg))).subscribe();*/
|
|
||||||
this.message.getChannel().flatMap(ch ->
|
|
||||||
ch.createMessage(this.message.getAuthor().map(u -> DPUtils.nickMention(u.getId()) + ", ").orElse("")
|
|
||||||
+ msg)).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(String[] message) {
|
|
||||||
sendMessage(String.join("\n", message));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return message.getAuthor().map(User::getUsername).orElse("Discord");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayerBase;
|
|
||||||
import com.google.common.collect.HashBiMap;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.OfflinePlayer;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"Connect command", //
|
|
||||||
"This command lets you connect your account with a Minecraft account. This allows using the private Minecraft chat and other things.", //
|
|
||||||
})
|
|
||||||
public class ConnectCommand extends ICommand2DC {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key: Minecraft name<br>
|
|
||||||
* Value: Discord ID
|
|
||||||
*/
|
|
||||||
public static HashBiMap<String, String> WaitingToConnect = HashBiMap.create();
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender, String Minecraftname) {
|
|
||||||
val message = sender.getMessage();
|
|
||||||
val channel = message.getChannel().block();
|
|
||||||
val author = message.getAuthor().orElse(null);
|
|
||||||
if (author == null || channel == null) return true;
|
|
||||||
if (WaitingToConnect.inverse().containsKey(author.getId().asString())) {
|
|
||||||
channel.createMessage(
|
|
||||||
"Replacing " + WaitingToConnect.inverse().get(author.getId().asString()) + " with " + Minecraftname).subscribe();
|
|
||||||
WaitingToConnect.inverse().remove(author.getId().asString());
|
|
||||||
}
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname);
|
|
||||||
if (p == null) {
|
|
||||||
channel.createMessage("The specified Minecraft player cannot be found").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class);
|
|
||||||
DiscordPlayer dp = pl.getAs(DiscordPlayer.class);
|
|
||||||
if (dp != null && author.getId().asString().equals(dp.getDiscordID())) {
|
|
||||||
channel.createMessage("You already have this account connected.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
WaitingToConnect.put(p.getName(), author.getId().asString());
|
|
||||||
channel.createMessage(
|
|
||||||
"Alright! Now accept the connection in Minecraft from the account " + Minecraftname
|
|
||||||
+ " before the next server restart. You can also adjust the Minecraft name you want to connect to by running this command again.").subscribe();
|
|
||||||
if (p.isOnline())
|
|
||||||
((Player) p).sendMessage("§bTo connect with the Discord account " + author.getUsername() + "#"
|
|
||||||
+ author.getDiscriminator() + " do /discord accept");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.listeners.CommonListeners;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"Switches debug mode."
|
|
||||||
})
|
|
||||||
public class DebugCommand extends ICommand2DC {
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender) {
|
|
||||||
sender.getMessage().getAuthorAsMember()
|
|
||||||
.switchIfEmpty(sender.getMessage().getAuthor() //Support DMs
|
|
||||||
.map(u -> u.asMember(DiscordPlugin.mainServer.getId()))
|
|
||||||
.orElse(Mono.empty()))
|
|
||||||
.flatMap(m -> DiscordPlugin.plugin.modRole.get()
|
|
||||||
.map(mr -> m.getRoleIds().stream().anyMatch(r -> r.equals(mr.getId())))
|
|
||||||
.switchIfEmpty(Mono.fromSupplier(() -> DiscordPlugin.mainServer.getOwnerId().asLong() == m.getId().asLong()))) //Role not found
|
|
||||||
.onErrorReturn(false).subscribe(success -> {
|
|
||||||
if (success)
|
|
||||||
sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled"));
|
|
||||||
else
|
|
||||||
sender.sendMessage("you need to be a moderator to use this command.");
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"Help command", //
|
|
||||||
"Shows some info about a command or lists the available commands.", //
|
|
||||||
})
|
|
||||||
public class HelpCommand extends ICommand2DC {
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender, @Command2.TextArg @Command2.OptionalArg String args) {
|
|
||||||
if (args == null || args.length() == 0)
|
|
||||||
sender.sendMessage(getManager().getCommandsText());
|
|
||||||
else {
|
|
||||||
String[] ht = getManager().getHelpText(args);
|
|
||||||
if (ht == null)
|
|
||||||
sender.sendMessage("Command not found: " + args);
|
|
||||||
else
|
|
||||||
sender.sendMessage(ht);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import buttondevteam.lib.chat.ICommand2;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
public abstract class ICommand2DC extends ICommand2<Command2DCSender> {
|
|
||||||
public <T extends ICommand2> ICommand2DC() {
|
|
||||||
super(DiscordPlugin.plugin.getManager());
|
|
||||||
val ann = getClass().getAnnotation(CommandClass.class);
|
|
||||||
if (ann == null)
|
|
||||||
modOnly = false;
|
|
||||||
else
|
|
||||||
modOnly = ann.modOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final @Getter boolean modOnly;
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase.InfoTarget;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"User information", //
|
|
||||||
"Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.", //
|
|
||||||
"If used without args, shows your info.", //
|
|
||||||
})
|
|
||||||
public class UserinfoCommand extends ICommand2DC {
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) {
|
|
||||||
val message = sender.getMessage();
|
|
||||||
User target = null;
|
|
||||||
val channel = message.getChannel().block();
|
|
||||||
assert channel != null;
|
|
||||||
if (user == null || user.length() == 0)
|
|
||||||
target = message.getAuthor().orElse(null);
|
|
||||||
else {
|
|
||||||
final User firstmention = message.getUserMentions()
|
|
||||||
.filter(m -> !m.getId().asString().equals(DiscordPlugin.dc.getSelfId().asString())).blockFirst();
|
|
||||||
if (firstmention != null)
|
|
||||||
target = firstmention;
|
|
||||||
else if (user.contains("#")) {
|
|
||||||
String[] targettag = user.split("#");
|
|
||||||
final List<User> targets = getUsers(message, targettag[0]);
|
|
||||||
if (targets.size() == 0) {
|
|
||||||
channel.createMessage("The user cannot be found (by name): " + user).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (User ptarget : targets) {
|
|
||||||
if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) {
|
|
||||||
target = ptarget;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target == null) {
|
|
||||||
channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size()
|
|
||||||
+ " users with the name.)").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final List<User> targets = getUsers(message, user);
|
|
||||||
if (targets.size() == 0) {
|
|
||||||
channel.createMessage("The user cannot be found on Discord: " + user).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (targets.size() > 1) {
|
|
||||||
channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
target = targets.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target == null) {
|
|
||||||
sender.sendMessage("An error occurred.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
DiscordPlayer dp = ChromaGamerBase.getUser(target.getId().asString(), DiscordPlayer.class);
|
|
||||||
StringBuilder uinfo = new StringBuilder("User info for ").append(target.getUsername()).append(":\n");
|
|
||||||
uinfo.append(dp.getInfo(InfoTarget.Discord));
|
|
||||||
channel.createMessage(uinfo.toString()).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<User> getUsers(Message message, String args) {
|
|
||||||
final List<User> targets;
|
|
||||||
val guild = message.getGuild().block();
|
|
||||||
if (guild == null) //Private channel
|
|
||||||
targets = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equalsIgnoreCase(args))
|
|
||||||
.collectList().block();
|
|
||||||
else
|
|
||||||
targets = guild.getMembers().filter(m -> m.getUsername().equalsIgnoreCase(args))
|
|
||||||
.map(m -> (User) m).collectList().block();
|
|
||||||
return targets;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package buttondevteam.discordplugin.commands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"Version",
|
|
||||||
"Returns the plugin's version"
|
|
||||||
})
|
|
||||||
public class VersionCommand extends ICommand2DC {
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender) {
|
|
||||||
sender.sendMessage(getVersion());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] getVersion() {
|
|
||||||
val desc = DiscordPlugin.plugin.getDescription();
|
|
||||||
return new String[]{ //
|
|
||||||
desc.getFullName(), //
|
|
||||||
desc.getWebsite() //
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
package buttondevteam.discordplugin.exceptions;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.TBMCDebugMessageEvent;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class DebugMessageListener implements Listener {
|
|
||||||
@EventHandler
|
|
||||||
public void onDebugMessage(TBMCDebugMessageEvent e) {
|
|
||||||
SendMessage(e.getDebugMessage());
|
|
||||||
e.setSent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendMessage(String message) {
|
|
||||||
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class))
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
Mono<MessageChannel> mc = ExceptionListenerModule.getChannel();
|
|
||||||
if (mc == null) return;
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("```").append("\n");
|
|
||||||
if (message.length() > 2000)
|
|
||||||
message = message.substring(0, 2000);
|
|
||||||
sb.append(message).append("\n");
|
|
||||||
sb.append("```");
|
|
||||||
mc.flatMap(ch -> ch.createMessage(sb.toString())).subscribe();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
package buttondevteam.discordplugin.exceptions;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.TBMCExceptionEvent;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import discord4j.core.object.entity.Guild;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.entity.channel.GuildChannel;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy.
|
|
||||||
*/
|
|
||||||
public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener {
|
|
||||||
private final List<Throwable> lastthrown = new ArrayList<>();
|
|
||||||
private final List<String> lastsourcemsg = new ArrayList<>();
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onException(TBMCExceptionEvent e) {
|
|
||||||
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass()))
|
|
||||||
return;
|
|
||||||
if (lastthrown.stream()
|
|
||||||
.anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace())
|
|
||||||
&& (e.getException().getMessage() == null ? ex.getMessage() == null
|
|
||||||
: e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message
|
|
||||||
&& lastsourcemsg.contains(e.getSourceMessage()))
|
|
||||||
return;
|
|
||||||
SendException(e.getException(), e.getSourceMessage());
|
|
||||||
if (lastthrown.size() >= 10)
|
|
||||||
lastthrown.remove(0);
|
|
||||||
if (lastsourcemsg.size() >= 10)
|
|
||||||
lastsourcemsg.remove(0);
|
|
||||||
lastthrown.add(e.getException());
|
|
||||||
lastsourcemsg.add(e.getSourceMessage());
|
|
||||||
e.setHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendException(Throwable e, String sourcemessage) {
|
|
||||||
if (instance == null) return;
|
|
||||||
try {
|
|
||||||
getChannel().flatMap(channel -> {
|
|
||||||
Mono<Role> coderRole;
|
|
||||||
if (channel instanceof GuildChannel)
|
|
||||||
coderRole = instance.pingRole(((GuildChannel) channel).getGuild()).get();
|
|
||||||
else
|
|
||||||
coderRole = Mono.empty();
|
|
||||||
return coderRole.map(role -> TBMCCoreAPI.IsTestServer() ? new StringBuilder()
|
|
||||||
: new StringBuilder(role.getMention()).append("\n"))
|
|
||||||
.defaultIfEmpty(new StringBuilder())
|
|
||||||
.flatMap(sb -> {
|
|
||||||
sb.append(sourcemessage).append("\n");
|
|
||||||
sb.append("```").append("\n");
|
|
||||||
String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n"))
|
|
||||||
.filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam."))
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
if (sb.length() + stackTrace.length() >= 1980)
|
|
||||||
stackTrace = stackTrace.substring(0, 1980 - sb.length());
|
|
||||||
sb.append(stackTrace).append("\n");
|
|
||||||
sb.append("```");
|
|
||||||
return channel.createMessage(sb.toString());
|
|
||||||
});
|
|
||||||
}).subscribe();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ExceptionListenerModule instance;
|
|
||||||
|
|
||||||
public static Mono<MessageChannel> getChannel() {
|
|
||||||
if (instance != null) return instance.channel.get();
|
|
||||||
return Mono.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel to post the errors to.
|
|
||||||
*/
|
|
||||||
private final ReadOnlyConfigData<Mono<MessageChannel>> channel = DPUtils.channelData(getConfig(), "channel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The role to ping if an error occurs. Set to empty ('') to disable.
|
|
||||||
*/
|
|
||||||
private ConfigData<Mono<Role>> pingRole(Mono<Guild> guild) {
|
|
||||||
return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
if (DPUtils.disableIfConfigError(this, channel)) return;
|
|
||||||
instance = this;
|
|
||||||
Bukkit.getPluginManager().registerEvents(new ExceptionListenerModule(), getPlugin());
|
|
||||||
TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener(), getPlugin());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
package buttondevteam.discordplugin.fun;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import discord4j.core.event.domain.PresenceUpdateEvent;
|
|
||||||
import discord4j.core.object.entity.Guild;
|
|
||||||
import discord4j.core.object.entity.Member;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.entity.channel.GuildChannel;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.core.object.presence.Status;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All kinds of random things.
|
|
||||||
* The YEEHAW event uses an emoji named :YEEHAW: if available
|
|
||||||
*/
|
|
||||||
public class FunModule extends Component<DiscordPlugin> implements Listener {
|
|
||||||
private static final String[] serverReadyStrings = new String[]{"in one week from now", // Ali
|
|
||||||
"between now and the heat-death of the universe.", // Ghostise
|
|
||||||
"soon™", "ask again this time next month", // Ghostise
|
|
||||||
"in about 3 seconds", // Nicolai
|
|
||||||
"after we finish 8 plugins", // Ali
|
|
||||||
"tomorrow.", // Ali
|
|
||||||
"after one tiiiny feature", // Ali
|
|
||||||
"next commit", // Ali
|
|
||||||
"after we finish strangling Towny", // Ali
|
|
||||||
"when we kill every *fucking* bug", // Ali
|
|
||||||
"once the server stops screaming.", // Ali
|
|
||||||
"after HL3 comes out", // Ali
|
|
||||||
"next time you ask", // Ali
|
|
||||||
"when will *you* be open?" // Ali
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Questions that the bot will choose a random answer to give to.
|
|
||||||
*/
|
|
||||||
private final ConfigData<String[]> serverReady = getConfig().getData("serverReady", () -> new String[]{
|
|
||||||
"when will the server be open", "when will the server be ready", "when will the server be done",
|
|
||||||
"when will the server be complete", "when will the server be finished", "when's the server ready",
|
|
||||||
"when's the server open", "vhen vill ze server be open?"});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Answers for a recognized question. Selected randomly.
|
|
||||||
*/
|
|
||||||
private final ConfigData<ArrayList<String>> serverReadyAnswers = getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings));
|
|
||||||
|
|
||||||
private static final Random serverReadyRandom = new Random();
|
|
||||||
private static final ArrayList<Short> usableServerReadyStrings = new ArrayList<>(0);
|
|
||||||
|
|
||||||
private void createUsableServerReadyStrings() {
|
|
||||||
IntStream.range(0, serverReadyAnswers.get().size())
|
|
||||||
.forEach(i -> FunModule.usableServerReadyStrings.add((short) i));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
registerListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
lastlist = lastlistp = ListC = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static short lastlist = 0;
|
|
||||||
private static short lastlistp = 0;
|
|
||||||
|
|
||||||
private static short ListC = 0;
|
|
||||||
|
|
||||||
public static boolean executeMemes(Message message) {
|
|
||||||
val fm = ComponentManager.getIfEnabled(FunModule.class);
|
|
||||||
if (fm == null) return false;
|
|
||||||
String msglowercased = message.getContent().toLowerCase();
|
|
||||||
lastlist++;
|
|
||||||
if (lastlist > 5) {
|
|
||||||
ListC = 0;
|
|
||||||
lastlist = 0;
|
|
||||||
}
|
|
||||||
if (msglowercased.equals("/list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already
|
|
||||||
{
|
|
||||||
DPUtils.reply(message, Mono.empty(), "stop it. You know the answer.").subscribe();
|
|
||||||
lastlist = 0;
|
|
||||||
lastlistp = (short) Bukkit.getOnlinePlayers().size();
|
|
||||||
return true; //Handled
|
|
||||||
}
|
|
||||||
lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle
|
|
||||||
if (!TBMCCoreAPI.IsTestServer()
|
|
||||||
&& Arrays.stream(fm.serverReady.get()).anyMatch(msglowercased::contains)) {
|
|
||||||
int next;
|
|
||||||
if (usableServerReadyStrings.size() == 0)
|
|
||||||
fm.createUsableServerReadyStrings();
|
|
||||||
next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size()));
|
|
||||||
DPUtils.reply(message, Mono.empty(), fm.serverReadyAnswers.get().get(next)).subscribe();
|
|
||||||
return false; //Still process it as a command/mcchat if needed
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
||||||
ListC = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If all of the people who have this role are online, the bot will post a full house.
|
|
||||||
*/
|
|
||||||
private ConfigData<Mono<Role>> fullHouseDevRole(Mono<Guild> guild) {
|
|
||||||
return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel to post the full house to.
|
|
||||||
*/
|
|
||||||
private final ReadOnlyConfigData<Mono<MessageChannel>> fullHouseChannel = DPUtils.channelData(getConfig(), "fullHouseChannel");
|
|
||||||
|
|
||||||
private static long lasttime = 0;
|
|
||||||
|
|
||||||
public static void handleFullHouse(PresenceUpdateEvent event) {
|
|
||||||
val fm = ComponentManager.getIfEnabled(FunModule.class);
|
|
||||||
if (fm == null) return;
|
|
||||||
if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 != 0) return;
|
|
||||||
fm.fullHouseChannel.get()
|
|
||||||
.filter(ch -> ch instanceof GuildChannel)
|
|
||||||
.flatMap(channel -> fm.fullHouseDevRole(((GuildChannel) channel).getGuild()).get()
|
|
||||||
.filter(role -> event.getOld().map(p -> p.getStatus().equals(Status.OFFLINE)).orElse(false))
|
|
||||||
.filter(role -> !event.getCurrent().getStatus().equals(Status.OFFLINE))
|
|
||||||
.filterWhen(devrole -> event.getMember().flatMap(m -> m.getRoles()
|
|
||||||
.any(r -> r.getId().asLong() == devrole.getId().asLong())))
|
|
||||||
.filterWhen(devrole ->
|
|
||||||
event.getGuild().flatMapMany(g -> g.getMembers().filter(m -> m.getRoleIds().stream().anyMatch(s -> s.equals(devrole.getId()))))
|
|
||||||
.flatMap(Member::getPresence).all(pr -> !pr.getStatus().equals(Status.OFFLINE)))
|
|
||||||
.filter(devrole -> lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime())) //This should stay so it checks this last
|
|
||||||
.flatMap(devrole -> {
|
|
||||||
lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime());
|
|
||||||
return channel.createMessage(mcs -> mcs.setContent("Full house!").setEmbed(ecs ->
|
|
||||||
ecs.setImage(
|
|
||||||
"https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png")
|
|
||||||
));
|
|
||||||
})).subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
package buttondevteam.discordplugin.listeners;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.commands.Command2DCSender;
|
|
||||||
import buttondevteam.discordplugin.util.Timings;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import lombok.val;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public class CommandListener {
|
|
||||||
/**
|
|
||||||
* Runs a ChromaBot command. If mentionedonly is false, it will only execute the command if it was in #bot with the correct prefix or in private.
|
|
||||||
*
|
|
||||||
* @param message The Discord message
|
|
||||||
* @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message
|
|
||||||
* @return Whether it <b>did not run</b> the command
|
|
||||||
*/
|
|
||||||
public static Mono<Boolean> runCommand(Message message, Snowflake commandChannelID, boolean mentionedonly) {
|
|
||||||
Timings timings = CommonListeners.timings;
|
|
||||||
Mono<Boolean> ret = Mono.just(true);
|
|
||||||
if (message.getContent().length() == 0)
|
|
||||||
return ret; //Pin messages and such, let the mcchat listener deal with it
|
|
||||||
val content = message.getContent();
|
|
||||||
timings.printElapsed("A");
|
|
||||||
return message.getChannel().flatMap(channel -> {
|
|
||||||
Mono<?> tmp = ret;
|
|
||||||
if (!mentionedonly) { //mentionedonly conditions are in CommonListeners
|
|
||||||
timings.printElapsed("B");
|
|
||||||
if (!(channel instanceof PrivateChannel)
|
|
||||||
&& !(content.charAt(0) == DiscordPlugin.getPrefix()
|
|
||||||
&& channel.getId().asLong() == commandChannelID.asLong())) //
|
|
||||||
return ret;
|
|
||||||
timings.printElapsed("C");
|
|
||||||
tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x)
|
|
||||||
}
|
|
||||||
final StringBuilder cmdwithargs = new StringBuilder(content);
|
|
||||||
val gotmention = new AtomicBoolean();
|
|
||||||
timings.printElapsed("Before self");
|
|
||||||
return tmp.flatMapMany(x ->
|
|
||||||
DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId()))
|
|
||||||
.flatMapMany(self -> {
|
|
||||||
timings.printElapsed("D");
|
|
||||||
gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message));
|
|
||||||
gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get());
|
|
||||||
val mentions = message.getRoleMentions();
|
|
||||||
return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName())))
|
|
||||||
.map(Role::getMention);
|
|
||||||
}).map(mentionRole -> {
|
|
||||||
timings.printElapsed("E");
|
|
||||||
gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get()); // Delete all mentions
|
|
||||||
return !mentionedonly || gotmention.get(); //Stops here if false
|
|
||||||
}).switchIfEmpty(Mono.fromSupplier(() -> !mentionedonly || gotmention.get())))
|
|
||||||
.filter(b -> b).last(false).filter(b -> b).doOnNext(b -> channel.type().subscribe()).flatMap(b -> {
|
|
||||||
String cmdwithargsString = cmdwithargs.toString();
|
|
||||||
try {
|
|
||||||
timings.printElapsed("F");
|
|
||||||
if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString))
|
|
||||||
return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.")
|
|
||||||
.map(m -> false);
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e, DiscordPlugin.plugin);
|
|
||||||
}
|
|
||||||
return Mono.just(false); //If the command succeeded or there was an error, return false
|
|
||||||
}).defaultIfEmpty(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) {
|
|
||||||
final char prefix = DiscordPlugin.getPrefix();
|
|
||||||
if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text
|
|
||||||
if (cmdwithargs.length() > mention.length() + 1) {
|
|
||||||
int i = cmdwithargs.indexOf(" ", mention.length());
|
|
||||||
if (i == -1)
|
|
||||||
i = mention.length();
|
|
||||||
else
|
|
||||||
//noinspection StatementWithEmptyBody
|
|
||||||
for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++)
|
|
||||||
; //Removes any space before the command
|
|
||||||
cmdwithargs.delete(0, i);
|
|
||||||
cmdwithargs.insert(0, prefix); //Always use the prefix for processing
|
|
||||||
} else
|
|
||||||
cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help");
|
|
||||||
else {
|
|
||||||
if (cmdwithargs.length() == 0)
|
|
||||||
cmdwithargs.replace(0, 0, prefix + "help");
|
|
||||||
else if (cmdwithargs.charAt(0) != prefix)
|
|
||||||
cmdwithargs.insert(0, prefix);
|
|
||||||
return false; //Don't treat / as mention, mentions can be used in public mcchat
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
package buttondevteam.discordplugin.listeners;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.fun.FunModule;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.role.GameRoleModule;
|
|
||||||
import buttondevteam.discordplugin.util.Timings;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import discord4j.core.event.EventDispatcher;
|
|
||||||
import discord4j.core.event.domain.PresenceUpdateEvent;
|
|
||||||
import discord4j.core.event.domain.message.MessageCreateEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleCreateEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleDeleteEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleUpdateEvent;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import lombok.val;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class CommonListeners {
|
|
||||||
|
|
||||||
public static final Timings timings = new Timings();
|
|
||||||
|
|
||||||
/*
|
|
||||||
MentionEvent:
|
|
||||||
- CommandListener (starts with mention, only 'channelcon' and not in #bot)
|
|
||||||
|
|
||||||
MessageReceivedEvent:
|
|
||||||
- v CommandListener (starts with mention, in #bot or a connected chat)
|
|
||||||
- Minecraft chat (is enabled in the channel and message isn't [/]mcchat)
|
|
||||||
- CommandListener (with the correct prefix in #bot, or in private)
|
|
||||||
*/
|
|
||||||
public static void register(EventDispatcher dispatcher) {
|
|
||||||
dispatcher.on(MessageCreateEvent.class).flatMap(event -> {
|
|
||||||
timings.printElapsed("Message received");
|
|
||||||
val def = Mono.empty();
|
|
||||||
if (DiscordPlugin.SafeMode)
|
|
||||||
return def;
|
|
||||||
val author = event.getMessage().getAuthor();
|
|
||||||
if (!author.isPresent() || author.get().isBot())
|
|
||||||
return def;
|
|
||||||
if (FunModule.executeMemes(event.getMessage()))
|
|
||||||
return def;
|
|
||||||
val commandChannel = DiscordPlugin.plugin.commandChannel.get();
|
|
||||||
return event.getMessage().getChannel().map(mch ->
|
|
||||||
(commandChannel != null && mch.getId().asLong() == commandChannel.asLong()) //If mentioned, that's higher than chat
|
|
||||||
|| mch instanceof PrivateChannel
|
|
||||||
|| event.getMessage().getContent().contains("channelcon")) //Only 'channelcon' is allowed in other channels
|
|
||||||
.flatMap(shouldRun -> { //Only continue if this doesn't handle the event
|
|
||||||
if (!shouldRun)
|
|
||||||
return Mono.just(true); //The condition is only for the first command execution, not mcchat
|
|
||||||
timings.printElapsed("Run command 1");
|
|
||||||
return CommandListener.runCommand(event.getMessage(), commandChannel, true); //#bot is handled here
|
|
||||||
}).filterWhen(ch -> {
|
|
||||||
timings.printElapsed("mcchat");
|
|
||||||
val mcchat = Component.getComponents().get(MinecraftChatModule.class);
|
|
||||||
if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again
|
|
||||||
return ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels
|
|
||||||
return Mono.just(true); //Wasn't handled, continue
|
|
||||||
}).filterWhen(ch -> {
|
|
||||||
timings.printElapsed("Run command 2");
|
|
||||||
return CommandListener.runCommand(event.getMessage(), commandChannel, false);
|
|
||||||
});
|
|
||||||
}).onErrorContinue((err, obj) -> TBMCCoreAPI.SendException("An error occured while handling a message!", err, DiscordPlugin.plugin))
|
|
||||||
.subscribe();
|
|
||||||
dispatcher.on(PresenceUpdateEvent.class).subscribe(event -> {
|
|
||||||
if (DiscordPlugin.SafeMode)
|
|
||||||
return;
|
|
||||||
FunModule.handleFullHouse(event);
|
|
||||||
});
|
|
||||||
dispatcher.on(RoleCreateEvent.class).subscribe(GameRoleModule::handleRoleEvent);
|
|
||||||
dispatcher.on(RoleDeleteEvent.class).subscribe(GameRoleModule::handleRoleEvent);
|
|
||||||
dispatcher.on(RoleUpdateEvent.class).subscribe(GameRoleModule::handleRoleEvent);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean debug = false;
|
|
||||||
|
|
||||||
public static void debug(String debug) {
|
|
||||||
if (CommonListeners.debug) //Debug
|
|
||||||
DPUtils.getLogger().info(debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean debug() {
|
|
||||||
return debug = !debug;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package buttondevteam.discordplugin.listeners;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.commands.ConnectCommand;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.util.DPState;
|
|
||||||
import buttondevteam.lib.ScheduledServerRestartEvent;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayerGetInfoEvent;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.Member;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class MCListener implements Listener {
|
|
||||||
@EventHandler
|
|
||||||
public void onPlayerJoin(PlayerJoinEvent e) {
|
|
||||||
if (ConnectCommand.WaitingToConnect.containsKey(e.getPlayer().getName())) {
|
|
||||||
@SuppressWarnings("ConstantConditions") User user = DiscordPlugin.dc
|
|
||||||
.getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.getPlayer().getName()))).block();
|
|
||||||
if (user == null) return;
|
|
||||||
e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getUsername() + "#" + user.getDiscriminator()
|
|
||||||
+ " do /discord accept");
|
|
||||||
e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onGetInfo(TBMCPlayerGetInfoEvent e) {
|
|
||||||
if (DiscordPlugin.SafeMode)
|
|
||||||
return;
|
|
||||||
DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class);
|
|
||||||
if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals(""))
|
|
||||||
return;
|
|
||||||
val userOpt = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).onErrorResume(t -> Mono.empty()).blockOptional();
|
|
||||||
if (!userOpt.isPresent()) return;
|
|
||||||
User user = userOpt.get();
|
|
||||||
e.addInfo("Discord tag: " + user.getUsername() + "#" + user.getDiscriminator());
|
|
||||||
val memberOpt = user.asMember(DiscordPlugin.mainServer.getId()).onErrorResume(t -> Mono.empty()).blockOptional();
|
|
||||||
if (!memberOpt.isPresent()) return;
|
|
||||||
Member member = memberOpt.get();
|
|
||||||
val prOpt = member.getPresence().blockOptional();
|
|
||||||
if (!prOpt.isPresent()) return;
|
|
||||||
val pr = prOpt.get();
|
|
||||||
e.addInfo(pr.getStatus().toString());
|
|
||||||
if (pr.getActivity().isPresent()) {
|
|
||||||
val activity = pr.getActivity().get();
|
|
||||||
e.addInfo(activity.getType() + ": " + activity.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*@EventHandler
|
|
||||||
public void onCommandPreprocess(TBMCCommandPreprocessEvent e) {
|
|
||||||
if (e.getMessage().equalsIgnoreCase("/stop"))
|
|
||||||
MinecraftChatModule.state = DPState.STOPPING_SERVER;
|
|
||||||
else if (e.getMessage().equalsIgnoreCase("/restart"))
|
|
||||||
MinecraftChatModule.state = DPState.RESTARTING_SERVER;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@EventHandler //We don't really need this with the logger stuff but hey
|
|
||||||
public void onScheduledRestart(ScheduledServerRestartEvent e) {
|
|
||||||
MinecraftChatModule.state = DPState.RESTARTING_SERVER;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.component.channel.Channel;
|
|
||||||
import buttondevteam.core.component.channel.ChatRoom;
|
|
||||||
import buttondevteam.discordplugin.*;
|
|
||||||
import buttondevteam.discordplugin.commands.Command2DCSender;
|
|
||||||
import buttondevteam.discordplugin.commands.ICommand2DC;
|
|
||||||
import buttondevteam.lib.TBMCSystemChatEvent;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.GuildChannel;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.rest.util.Permission;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@SuppressWarnings("SimplifyOptionalCallChains") //Java 11
|
|
||||||
@CommandClass(helpText = {"Channel connect", //
|
|
||||||
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
|
|
||||||
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
|
|
||||||
"You also need to have your Minecraft account connected. In #bot use /connect <mcname>.", //
|
|
||||||
"Call this command from the channel you want to use.", //
|
|
||||||
"Usage: @Bot channelcon <mcchannel>", //
|
|
||||||
"Use the ID (command) of the channel, for example `g` for the global chat.", //
|
|
||||||
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
|
|
||||||
"Mentioning the bot is needed in this case because the / prefix only works in #bot.", //
|
|
||||||
"Invite link: <Unknown>" //
|
|
||||||
})
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ChannelconCommand extends ICommand2DC {
|
|
||||||
private final MinecraftChatModule module;
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean remove(Command2DCSender sender) {
|
|
||||||
val message = sender.getMessage();
|
|
||||||
if (checkPerms(message, null)) return true;
|
|
||||||
if (MCChatCustom.removeCustomChat(message.getChannelId()))
|
|
||||||
DPUtils.reply(message, Mono.empty(), "channel connection removed.").subscribe();
|
|
||||||
else
|
|
||||||
DPUtils.reply(message, Mono.empty(), "this channel isn't connected.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) {
|
|
||||||
val message = sender.getMessage();
|
|
||||||
if (checkPerms(message, null)) return true;
|
|
||||||
val cc = MCChatCustom.getCustomChat(message.getChannelId());
|
|
||||||
if (cc == null)
|
|
||||||
return respond(sender, "this channel isn't connected.");
|
|
||||||
Supplier<String> togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n"))
|
|
||||||
+ "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n"));
|
|
||||||
if (toggle == null) {
|
|
||||||
DPUtils.reply(message, Mono.empty(), "toggles:\n" + togglesString.get()).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String arg = toggle.toUpperCase();
|
|
||||||
val b = Arrays.stream(ChannelconBroadcast.values()).filter(t -> t.toString().equals(arg)).findAny();
|
|
||||||
if (!b.isPresent()) {
|
|
||||||
val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg);
|
|
||||||
if (bt == null) {
|
|
||||||
DPUtils.reply(message, Mono.empty(), "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final boolean add;
|
|
||||||
if (add = !cc.brtoggles.contains(bt))
|
|
||||||
cc.brtoggles.add(bt);
|
|
||||||
else
|
|
||||||
cc.brtoggles.remove(bt);
|
|
||||||
return respond(sender, "'" + bt.getName() + "' " + (add ? "en" : "dis") + "abled");
|
|
||||||
}
|
|
||||||
//A B | F
|
|
||||||
//------- A: original - B: mask - F: new
|
|
||||||
//0 0 | 0
|
|
||||||
//0 1 | 1
|
|
||||||
//1 0 | 1
|
|
||||||
//1 1 | 0
|
|
||||||
// XOR
|
|
||||||
cc.toggles ^= b.get().flag;
|
|
||||||
DPUtils.reply(message, Mono.empty(), "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender, String channelID) {
|
|
||||||
val message = sender.getMessage();
|
|
||||||
if (!module.allowCustomChat.get()) {
|
|
||||||
sender.sendMessage("channel connection is not allowed on this Minecraft server.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
val channel = message.getChannel().block();
|
|
||||||
if (checkPerms(message, channel)) return true;
|
|
||||||
if (MCChatCustom.hasCustomChat(message.getChannelId()))
|
|
||||||
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.");
|
|
||||||
val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs.get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny();
|
|
||||||
if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
|
|
||||||
DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!message.getAuthor().isPresent()) return true;
|
|
||||||
val author = message.getAuthor().get();
|
|
||||||
val dp = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class);
|
|
||||||
val chp = dp.getAs(TBMCPlayer.class);
|
|
||||||
if (chp == null) {
|
|
||||||
DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module);
|
|
||||||
//Using a fake player with no login/logout, should be fine for this event
|
|
||||||
String groupid = chan.get().getGroupID(dcp);
|
|
||||||
if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later
|
|
||||||
DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well
|
|
||||||
DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
|
|
||||||
DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm.");
|
|
||||||
return true;
|
|
||||||
}*/ //TODO: "Channel admins" that can connect channels?
|
|
||||||
MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>());
|
|
||||||
if (chan.get() instanceof ChatRoom)
|
|
||||||
DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe();
|
|
||||||
else
|
|
||||||
DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
private boolean checkPerms(Message message, @Nullable MessageChannel channel) {
|
|
||||||
if (channel == null)
|
|
||||||
channel = message.getChannel().block();
|
|
||||||
if (!(channel instanceof GuildChannel)) {
|
|
||||||
DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
//noinspection OptionalGetWithoutIsPresent
|
|
||||||
var perms = ((GuildChannel) channel).getEffectivePermissions(message.getAuthor().map(User::getId).get()).block();
|
|
||||||
if (!perms.contains(Permission.ADMINISTRATOR) && !perms.contains(Permission.MANAGE_CHANNELS)) {
|
|
||||||
DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getHelpText(Method method, Command2.Subcommand ann) {
|
|
||||||
return new String[]{ //
|
|
||||||
"Channel connect", //
|
|
||||||
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
|
|
||||||
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
|
|
||||||
"You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect <mcname>.", //
|
|
||||||
"Call this command from the channel you want to use.", //
|
|
||||||
"Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon <mcchannel>", //
|
|
||||||
"Use the ID (command) of the channel, for example `g` for the global chat.", //
|
|
||||||
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
|
|
||||||
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", //
|
|
||||||
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=" + DiscordPlugin.dc.getApplicationInfo().map(info -> info.getId().asString()).blockOptional().orElse("Unknown") + "&scope=bot&permissions=268509264>"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.commands.Command2DCSender;
|
|
||||||
import buttondevteam.discordplugin.commands.ICommand2DC;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
@CommandClass(helpText = {
|
|
||||||
"MC Chat",
|
|
||||||
"This command enables or disables the Minecraft chat in private messages.", //
|
|
||||||
"It can be useful if you don't want your messages to be visible, for example when talking in a private channel.", //
|
|
||||||
"You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." //
|
|
||||||
})
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MCChatCommand extends ICommand2DC {
|
|
||||||
|
|
||||||
private final MinecraftChatModule module;
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean def(Command2DCSender sender) {
|
|
||||||
if (!module.allowPrivateChat.get()) {
|
|
||||||
sender.sendMessage("using the private chat is not allowed on this Minecraft server.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
val message = sender.getMessage();
|
|
||||||
val channel = message.getChannel().block();
|
|
||||||
@SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get();
|
|
||||||
if (!(channel instanceof PrivateChannel)) {
|
|
||||||
DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class);
|
|
||||||
boolean mcchat = !user.isMinecraftChatEnabled();
|
|
||||||
MCChatPrivate.privateMCChat(channel, mcchat, author, user);
|
|
||||||
DPUtils.reply(message, channel, "Minecraft chat " + (mcchat //
|
|
||||||
? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." //
|
|
||||||
: "disabled.")).subscribe();
|
|
||||||
return true;
|
|
||||||
} // TODO: Pin channel switching to indicate the current channel
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.component.channel.Channel;
|
|
||||||
import buttondevteam.core.component.channel.ChatRoom;
|
|
||||||
import buttondevteam.discordplugin.DiscordConnectedPlayer;
|
|
||||||
import buttondevteam.lib.TBMCSystemChatEvent;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class MCChatCustom {
|
|
||||||
/**
|
|
||||||
* Used for town or nation chats or anything else
|
|
||||||
*/
|
|
||||||
static final ArrayList<CustomLMD> lastmsgCustom = new ArrayList<>();
|
|
||||||
|
|
||||||
public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
|
|
||||||
synchronized (lastmsgCustom) {
|
|
||||||
if (mcchannel instanceof ChatRoom) {
|
|
||||||
((ChatRoom) mcchannel).joinRoom(dcp);
|
|
||||||
if (groupid == null) groupid = mcchannel.getGroupID(dcp);
|
|
||||||
}
|
|
||||||
val lmd = new CustomLMD(channel, user, groupid, mcchannel, dcp, toggles, brtoggles);
|
|
||||||
lastmsgCustom.add(lmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasCustomChat(Snowflake channel) {
|
|
||||||
return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getId().asLong() == channel.asLong());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static CustomLMD getCustomChat(Snowflake channel) {
|
|
||||||
return lastmsgCustom.stream().filter(lmd -> lmd.channel.getId().asLong() == channel.asLong()).findAny().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean removeCustomChat(Snowflake channel) {
|
|
||||||
synchronized (lastmsgCustom) {
|
|
||||||
MCChatUtils.lastmsgfromd.remove(channel.asLong());
|
|
||||||
return lastmsgCustom.removeIf(lmd -> {
|
|
||||||
if (lmd.channel.getId().asLong() != channel.asLong())
|
|
||||||
return false;
|
|
||||||
if (lmd.mcchannel instanceof ChatRoom)
|
|
||||||
((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CustomLMD> getCustomChats() {
|
|
||||||
return Collections.unmodifiableList(lastmsgCustom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CustomLMD extends MCChatUtils.LastMsgData {
|
|
||||||
public final String groupID;
|
|
||||||
public final DiscordConnectedPlayer dcp;
|
|
||||||
public int toggles;
|
|
||||||
public Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles;
|
|
||||||
|
|
||||||
private CustomLMD(@NonNull MessageChannel channel, @NonNull User user,
|
|
||||||
@NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
|
|
||||||
super(channel, user);
|
|
||||||
groupID = groupid;
|
|
||||||
this.mcchannel = mcchannel;
|
|
||||||
this.dcp = dcp;
|
|
||||||
this.toggles = toggles;
|
|
||||||
this.brtoggles = brtoggles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,417 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.*;
|
|
||||||
import buttondevteam.discordplugin.listeners.CommandListener;
|
|
||||||
import buttondevteam.discordplugin.listeners.CommonListeners;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener14;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener15;
|
|
||||||
import buttondevteam.discordplugin.util.Timings;
|
|
||||||
import buttondevteam.lib.*;
|
|
||||||
import buttondevteam.lib.chat.ChatMessage;
|
|
||||||
import buttondevteam.lib.chat.TBMCChatAPI;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import com.vdurmont.emoji.EmojiParser;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.event.domain.message.MessageCreateEvent;
|
|
||||||
import discord4j.core.object.Embed;
|
|
||||||
import discord4j.core.object.entity.Attachment;
|
|
||||||
import discord4j.core.object.entity.Guild;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.GuildChannel;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import discord4j.core.spec.EmbedCreateSpec;
|
|
||||||
import discord4j.rest.util.Color;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.AbstractMap;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class MCChatListener implements Listener {
|
|
||||||
private BukkitTask sendtask;
|
|
||||||
private final LinkedBlockingQueue<AbstractMap.SimpleEntry<TBMCChatEvent, Instant>> sendevents = new LinkedBlockingQueue<>();
|
|
||||||
private Runnable sendrunnable;
|
|
||||||
private Thread sendthread;
|
|
||||||
private final MinecraftChatModule module;
|
|
||||||
private boolean stop = false; //A new instance will be created on enable
|
|
||||||
|
|
||||||
public MCChatListener(MinecraftChatModule minecraftChatModule) {
|
|
||||||
module = minecraftChatModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler // Minecraft
|
|
||||||
public void onMCChat(TBMCChatEvent ev) {
|
|
||||||
if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown
|
|
||||||
return;
|
|
||||||
sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now()));
|
|
||||||
if (sendtask != null)
|
|
||||||
return;
|
|
||||||
sendrunnable = () -> {
|
|
||||||
sendthread = Thread.currentThread();
|
|
||||||
processMCToDiscord();
|
|
||||||
if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down
|
|
||||||
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
|
|
||||||
};
|
|
||||||
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processMCToDiscord() {
|
|
||||||
try {
|
|
||||||
TBMCChatEvent e;
|
|
||||||
Instant time;
|
|
||||||
val se = sendevents.take(); // Wait until an element is available
|
|
||||||
e = se.getKey();
|
|
||||||
time = se.getValue();
|
|
||||||
|
|
||||||
final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName.get()) + "] " //
|
|
||||||
+ ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().charAt(0) + "]") //
|
|
||||||
+ (DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender())));
|
|
||||||
val color = e.getChannel().Color.get();
|
|
||||||
final Consumer<EmbedCreateSpec> embed = ecs -> {
|
|
||||||
ecs.setDescription(e.getMessage()).setColor(Color.of(color.getRed(),
|
|
||||||
color.getGreen(), color.getBlue()));
|
|
||||||
String url = module.profileURL.get();
|
|
||||||
if (e.getSender() instanceof Player)
|
|
||||||
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(),
|
|
||||||
url.length() > 0 ? url + "?type=minecraft&id="
|
|
||||||
+ ((Player) e.getSender()).getUniqueId() : null);
|
|
||||||
else if (e.getSender() instanceof DiscordSenderBase)
|
|
||||||
ecs.setAuthor(authorPlayer, url.length() > 0 ? url + "?type=discord&id="
|
|
||||||
+ ((DiscordSenderBase) e.getSender()).getUser().getId().asString() : null,
|
|
||||||
((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl());
|
|
||||||
else
|
|
||||||
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null);
|
|
||||||
ecs.setTimestamp(time);
|
|
||||||
};
|
|
||||||
final long nanoTime = System.nanoTime();
|
|
||||||
InterruptibleConsumer<MCChatUtils.LastMsgData> doit = lastmsgdata -> {
|
|
||||||
if (lastmsgdata.message == null
|
|
||||||
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null))
|
|
||||||
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|
|
||||||
|| !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)
|
|
||||||
|| lastmsgdata.content.length() + e.getMessage().length() + 1 > 2048) {
|
|
||||||
lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block();
|
|
||||||
lastmsgdata.time = nanoTime;
|
|
||||||
lastmsgdata.mcchannel = e.getChannel();
|
|
||||||
lastmsgdata.content = e.getMessage();
|
|
||||||
} else {
|
|
||||||
lastmsgdata.content = lastmsgdata.content + "\n"
|
|
||||||
+ e.getMessage(); // The message object doesn't get updated
|
|
||||||
lastmsgdata.message.edit(mes -> mes.setEmbed(embed.andThen(ecs ->
|
|
||||||
ecs.setDescription(lastmsgdata.content)))).block();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Checks if the given channel is different than where the message was sent from
|
|
||||||
// Or if it was from MC
|
|
||||||
Predicate<Snowflake> isdifferentchannel = id -> !(e.getSender() instanceof DiscordSenderBase)
|
|
||||||
|| ((DiscordSenderBase) e.getSender()).getChannel().getId().asLong() != id.asLong();
|
|
||||||
|
|
||||||
if (e.getChannel().isGlobal()
|
|
||||||
&& (e.isFromCommand() || isdifferentchannel.test(module.chatChannel.get())))
|
|
||||||
doit.accept(MCChatUtils.lastmsgdata == null
|
|
||||||
? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono().block(), null)
|
|
||||||
: MCChatUtils.lastmsgdata);
|
|
||||||
|
|
||||||
for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) {
|
|
||||||
if ((e.isFromCommand() || isdifferentchannel.test(data.channel.getId()))
|
|
||||||
&& e.shouldSendTo(MCChatUtils.getSender(data.channel.getId(), data.user)))
|
|
||||||
doit.accept(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (MCChatCustom.lastmsgCustom) {
|
|
||||||
val iterator = MCChatCustom.lastmsgCustom.iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val lmd = iterator.next();
|
|
||||||
if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel.getId())) //Test if msg is from Discord
|
|
||||||
&& e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it
|
|
||||||
&& e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58
|
|
||||||
if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions
|
|
||||||
doit.accept(lmd);
|
|
||||||
else {
|
|
||||||
iterator.remove(); //If the user no longer has permission, remove the connection
|
|
||||||
lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (InterruptedException ex) { //Stop if interrupted anywhere
|
|
||||||
sendtask.cancel();
|
|
||||||
sendtask = null;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onChatPreprocess(TBMCChatPreprocessEvent event) {
|
|
||||||
int start = -1;
|
|
||||||
while ((start = event.getMessage().indexOf('@', start + 1)) != -1) {
|
|
||||||
int mid = event.getMessage().indexOf('#', start + 1);
|
|
||||||
if (mid == -1)
|
|
||||||
return;
|
|
||||||
int end_ = event.getMessage().indexOf(' ', mid + 1);
|
|
||||||
if (end_ == -1)
|
|
||||||
end_ = event.getMessage().length();
|
|
||||||
final int end = end_;
|
|
||||||
final int startF = start;
|
|
||||||
val user = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equals(event.getMessage().substring(startF + 1, mid)))
|
|
||||||
.filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).blockFirst();
|
|
||||||
if (user != null) //TODO: Nicknames
|
|
||||||
event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getUsername()
|
|
||||||
+ (event.getMessage().length() > end ? event.getMessage().substring(end) : "")); // TODO: Add formatting
|
|
||||||
start = end; // Skip any @s inside the mention
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender
|
|
||||||
// Offline public chat......x............................................
|
|
||||||
// Online public chat.......x...........................................x
|
|
||||||
// Offline private chat.....x.......................x....................
|
|
||||||
// Online private chat......x.......................x...................x
|
|
||||||
// If online and enabling private chat, don't login
|
|
||||||
// If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority
|
|
||||||
// If private chat is enabled and joining the server, logout the fake player on highest priority
|
|
||||||
// If online and disabling private chat, don't logout
|
|
||||||
// The maps may not contain the senders for UnconnectedSenders
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the listener permanently. Enabling the module will create a new instance.
|
|
||||||
*
|
|
||||||
* @param wait Wait 5 seconds for the threads to stop
|
|
||||||
*/
|
|
||||||
public void stop(boolean wait) {
|
|
||||||
stop = true;
|
|
||||||
MCChatPrivate.logoutAll();
|
|
||||||
MCChatUtils.LoggedInPlayers.clear();
|
|
||||||
if (sendthread != null) sendthread.interrupt();
|
|
||||||
if (recthread != null) recthread.interrupt();
|
|
||||||
try {
|
|
||||||
if (sendthread != null) {
|
|
||||||
sendthread.interrupt();
|
|
||||||
if (wait)
|
|
||||||
sendthread.join(5000);
|
|
||||||
}
|
|
||||||
if (recthread != null) {
|
|
||||||
recthread.interrupt();
|
|
||||||
if (wait)
|
|
||||||
recthread.join(5000);
|
|
||||||
}
|
|
||||||
MCChatUtils.lastmsgdata = null;
|
|
||||||
MCChatPrivate.lastmsgPerUser.clear();
|
|
||||||
MCChatCustom.lastmsgCustom.clear();
|
|
||||||
MCChatUtils.lastmsgfromd.clear();
|
|
||||||
MCChatUtils.UnconnectedSenders.clear();
|
|
||||||
recthread = sendthread = null;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace(); //This thread shouldn't be interrupted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BukkitTask rectask;
|
|
||||||
private final LinkedBlockingQueue<MessageCreateEvent> recevents = new LinkedBlockingQueue<>();
|
|
||||||
private Runnable recrun;
|
|
||||||
private Thread recthread;
|
|
||||||
|
|
||||||
// Discord
|
|
||||||
public Mono<Boolean> handleDiscord(MessageCreateEvent ev) {
|
|
||||||
Timings timings = CommonListeners.timings;
|
|
||||||
timings.printElapsed("Chat event");
|
|
||||||
val author = ev.getMessage().getAuthor();
|
|
||||||
final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId());
|
|
||||||
var prefix = DiscordPlugin.getPrefix();
|
|
||||||
return ev.getMessage().getChannel().filter(channel -> {
|
|
||||||
timings.printElapsed("Filter 1");
|
|
||||||
return !(ev.getMessage().getChannelId().asLong() != module.chatChannel.get().asLong()
|
|
||||||
&& !(channel instanceof PrivateChannel
|
|
||||||
&& author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false))
|
|
||||||
&& !hasCustomChat); //Chat isn't enabled on this channel
|
|
||||||
}).filter(channel -> {
|
|
||||||
timings.printElapsed("Filter 2");
|
|
||||||
return !(channel instanceof PrivateChannel //Only in private chat
|
|
||||||
&& ev.getMessage().getContent().length() < "/mcchat<>".length()
|
|
||||||
&& ev.getMessage().getContent().replace(prefix + "", "")
|
|
||||||
.equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat
|
|
||||||
//Allow disabling the chat if needed
|
|
||||||
}).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), DiscordPlugin.plugin.commandChannel.get(), true))
|
|
||||||
//Allow running commands in chat channels
|
|
||||||
.filter(channel -> {
|
|
||||||
MCChatUtils.resetLastMessage(channel);
|
|
||||||
recevents.add(ev);
|
|
||||||
timings.printElapsed("Message event added");
|
|
||||||
if (rectask != null)
|
|
||||||
return true;
|
|
||||||
recrun = () -> { //Don't return in a while loop next time
|
|
||||||
recthread = Thread.currentThread();
|
|
||||||
processDiscordToMC();
|
|
||||||
if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down
|
|
||||||
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing
|
|
||||||
};
|
|
||||||
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing
|
|
||||||
return true;
|
|
||||||
}).map(b -> false).defaultIfEmpty(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processDiscordToMC() {
|
|
||||||
MessageCreateEvent event;
|
|
||||||
try {
|
|
||||||
event = recevents.take();
|
|
||||||
} catch (InterruptedException e1) {
|
|
||||||
rectask.cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val sender = event.getMessage().getAuthor().orElse(null);
|
|
||||||
String dmessage = event.getMessage().getContent();
|
|
||||||
try {
|
|
||||||
final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender);
|
|
||||||
val user = dsender.getChromaUser();
|
|
||||||
|
|
||||||
for (User u : event.getMessage().getUserMentions().toIterable()) { //TODO: Role mentions
|
|
||||||
dmessage = dmessage.replace(u.getMention(), "@" + u.getUsername()); // TODO: IG Formatting
|
|
||||||
val m = u.asMember(DiscordPlugin.mainServer.getId()).onErrorResume(t -> Mono.empty()).blockOptional();
|
|
||||||
if (m.isPresent()) {
|
|
||||||
val mm = m.get();
|
|
||||||
final String nick = mm.getDisplayName();
|
|
||||||
dmessage = dmessage.replace(mm.getNicknameMention(), "@" + nick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (GuildChannel ch : event.getGuild().flux().flatMap(Guild::getChannels).toIterable()) {
|
|
||||||
dmessage = dmessage.replace(ch.getMention(), "#" + ch.getName()); // TODO: IG Formatting
|
|
||||||
}
|
|
||||||
|
|
||||||
dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?)
|
|
||||||
dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up
|
|
||||||
|
|
||||||
dmessage = dmessage.replaceAll("<a?:(\\S+):(\\d+)>", ":$1:"); //We don't need info about the custom emojis, just display their text
|
|
||||||
|
|
||||||
Function<String, String> getChatMessage = msg -> //
|
|
||||||
msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage()
|
|
||||||
.getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n"))
|
|
||||||
: "");
|
|
||||||
|
|
||||||
MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getMessage().getChannelId());
|
|
||||||
|
|
||||||
boolean react = false;
|
|
||||||
|
|
||||||
val sendChannel = event.getMessage().getChannel().block();
|
|
||||||
boolean isPrivate = sendChannel instanceof PrivateChannel;
|
|
||||||
if (dmessage.startsWith("/")) { // Ingame command
|
|
||||||
if (handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)) return;
|
|
||||||
} else {// Not a command
|
|
||||||
react = handleIngameMessage(event, dmessage, dsender, user, getChatMessage, clmd, isPrivate);
|
|
||||||
}
|
|
||||||
if (react) {
|
|
||||||
try {
|
|
||||||
val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong());
|
|
||||||
if (lmfd != null) {
|
|
||||||
lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // Remove it no matter what, we know it's there 99.99% of the time
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module);
|
|
||||||
}
|
|
||||||
MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage());
|
|
||||||
event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean handleIngameMessage(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, Function<String, String> getChatMessage, MCChatCustom.CustomLMD clmd, boolean isPrivate) {
|
|
||||||
boolean react = false;
|
|
||||||
if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0
|
|
||||||
&& !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) {
|
|
||||||
val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp)
|
|
||||||
: dsender.getChromaUser().channel.get().getRTR(dsender);
|
|
||||||
TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel.get(), rtr,
|
|
||||||
(dsender instanceof Player ? ((Player) dsender).getDisplayName()
|
|
||||||
: dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL);
|
|
||||||
} else {
|
|
||||||
val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false);
|
|
||||||
if (clmd != null)
|
|
||||||
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel);
|
|
||||||
else
|
|
||||||
TBMCChatAPI.SendChatMessage(cmb.build());
|
|
||||||
react = true;
|
|
||||||
}
|
|
||||||
return react;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean handleIngameCommand(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, MCChatCustom.CustomLMD clmd, boolean isPrivate) {
|
|
||||||
if (!isPrivate)
|
|
||||||
event.getMessage().delete().subscribe();
|
|
||||||
final String cmd = dmessage.substring(1);
|
|
||||||
final String cmdlowercased = cmd.toLowerCase();
|
|
||||||
if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream()
|
|
||||||
.noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) {
|
|
||||||
// Command not whitelisted
|
|
||||||
dsender.sendMessage("Sorry, you can only access these commands from here:\n"
|
|
||||||
+ module.whitelistedCommands().get().stream().map(uc -> "/" + uc)
|
|
||||||
.collect(Collectors.joining(", "))
|
|
||||||
+ (user.getConnectedID(TBMCPlayer.class) == null
|
|
||||||
? "\nTo access your commands, first please connect your accounts, using /connect in "
|
|
||||||
+ DPUtils.botmention()
|
|
||||||
+ "\nThen y"
|
|
||||||
: "\nY")
|
|
||||||
+ "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
module.log(dsender.getName() + " ran from DC: /" + cmd);
|
|
||||||
if (dsender instanceof DiscordSender && runCustomCommand(dsender, cmdlowercased)) return true;
|
|
||||||
val channel = clmd == null ? user.channel.get() : clmd.mcchannel;
|
|
||||||
val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, clmd == null ? dsender : clmd.dcp);
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync
|
|
||||||
() -> {
|
|
||||||
Bukkit.getPluginManager().callEvent(ev);
|
|
||||||
if (ev.isCancelled())
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
String mcpackage = Bukkit.getServer().getClass().getPackage().getName();
|
|
||||||
if (!module.enableVanillaCommands.get())
|
|
||||||
Bukkit.dispatchCommand(dsender, cmd);
|
|
||||||
else if (mcpackage.contains("1_12"))
|
|
||||||
VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd);
|
|
||||||
else if (mcpackage.contains("1_14"))
|
|
||||||
VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd);
|
|
||||||
else if (mcpackage.contains("1_15") || mcpackage.contains("1_16"))
|
|
||||||
VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd);
|
|
||||||
else
|
|
||||||
Bukkit.dispatchCommand(dsender, cmd);
|
|
||||||
} catch (NoClassDefFoundError e) {
|
|
||||||
TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module);
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean runCustomCommand(DiscordSenderBase dsender, String cmdlowercased) {
|
|
||||||
if (cmdlowercased.startsWith("list")) {
|
|
||||||
var players = Bukkit.getOnlinePlayers();
|
|
||||||
dsender.sendMessage("There are " + players.stream().filter(MCChatUtils::checkEssentials).count() + " out of " + Bukkit.getMaxPlayers() + " players online.");
|
|
||||||
dsender.sendMessage("Players: " + players.stream().filter(MCChatUtils::checkEssentials)
|
|
||||||
.map(Player::getDisplayName).collect(Collectors.joining(", ")));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface InterruptibleConsumer<T> {
|
|
||||||
void accept(T value) throws TimeoutException, InterruptedException;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.DiscordConnectedPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class MCChatPrivate {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for messages in PMs (mcchat).
|
|
||||||
*/
|
|
||||||
static ArrayList<MCChatUtils.LastMsgData> lastmsgPerUser = new ArrayList<>();
|
|
||||||
|
|
||||||
public static boolean privateMCChat(MessageChannel channel, boolean start, User user, DiscordPlayer dp) {
|
|
||||||
synchronized (MCChatUtils.ConnectedSenders) {
|
|
||||||
TBMCPlayer mcp = dp.getAs(TBMCPlayer.class);
|
|
||||||
if (mcp != null) { // If the accounts aren't connected, can't make a connected sender
|
|
||||||
val p = Bukkit.getPlayer(mcp.getUUID());
|
|
||||||
val op = Bukkit.getOfflinePlayer(mcp.getUUID());
|
|
||||||
val mcm = ComponentManager.getIfEnabled(MinecraftChatModule.class);
|
|
||||||
if (start) {
|
|
||||||
val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID(), op.getName(), mcm);
|
|
||||||
MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender);
|
|
||||||
MCChatUtils.LoggedInPlayers.put(mcp.getUUID(), sender);
|
|
||||||
if (p == null) // Player is offline - If the player is online, that takes precedence
|
|
||||||
MCChatUtils.callLoginEvents(sender);
|
|
||||||
} else {
|
|
||||||
val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user);
|
|
||||||
assert sender != null;
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> {
|
|
||||||
if ((p == null || p instanceof DiscordSenderBase) // Player is offline - If the player is online, that takes precedence
|
|
||||||
&& sender.isLoggedIn()) //Don't call the quit event if login failed
|
|
||||||
MCChatUtils.callLogoutEvent(sender, false); //The next line has to run *after* this one, so can't use the needsSync parameter
|
|
||||||
MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId());
|
|
||||||
sender.setLoggedIn(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} // ---- PermissionsEx warning is normal on logout ----
|
|
||||||
if (!start)
|
|
||||||
MCChatUtils.lastmsgfromd.remove(channel.getId().asLong());
|
|
||||||
return start //
|
|
||||||
? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs
|
|
||||||
: lastmsgPerUser.removeIf(lmd -> lmd.channel.getId().asLong() == channel.getId().asLong());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isMinecraftChatEnabled(DiscordPlayer dp) {
|
|
||||||
return isMinecraftChatEnabled(dp.getDiscordID());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this
|
|
||||||
return lastmsgPerUser.stream()
|
|
||||||
.anyMatch(lmd -> ((PrivateChannel) lmd.channel)
|
|
||||||
.getRecipientIds().stream().anyMatch(u -> u.asString().equals(did)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void logoutAll() {
|
|
||||||
synchronized (MCChatUtils.ConnectedSenders) {
|
|
||||||
for (val entry : MCChatUtils.ConnectedSenders.entrySet())
|
|
||||||
for (val valueEntry : entry.getValue().entrySet())
|
|
||||||
if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey(), valueEntry.getValue().getUser()) == null) //If the player is online then the fake player was already logged out
|
|
||||||
MCChatUtils.callLogoutEvent(valueEntry.getValue(), !Bukkit.isPrimaryThread());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,410 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.core.MainPlugin;
|
|
||||||
import buttondevteam.discordplugin.*;
|
|
||||||
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.TBMCSystemChatEvent;
|
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.Message;
|
|
||||||
import discord4j.core.object.entity.User;
|
|
||||||
import discord4j.core.object.entity.channel.Channel;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.core.object.entity.channel.PrivateChannel;
|
|
||||||
import discord4j.core.object.entity.channel.TextChannel;
|
|
||||||
import io.netty.util.collection.LongObjectHashMap;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.event.Event;
|
|
||||||
import org.bukkit.event.HandlerList;
|
|
||||||
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
|
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
|
||||||
import org.bukkit.event.player.PlayerLoginEvent;
|
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
|
||||||
import org.bukkit.plugin.AuthorNagException;
|
|
||||||
import org.bukkit.plugin.Plugin;
|
|
||||||
import org.bukkit.plugin.RegisteredListener;
|
|
||||||
import org.reactivestreams.Publisher;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public class MCChatUtils {
|
|
||||||
/**
|
|
||||||
* May contain P<DiscordID> as key for public chat
|
|
||||||
*/
|
|
||||||
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordSender>> UnconnectedSenders = new ConcurrentHashMap<>();
|
|
||||||
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordConnectedPlayer>> ConnectedSenders = new ConcurrentHashMap<>();
|
|
||||||
/**
|
|
||||||
* May contain P<DiscordID> as key for public chat
|
|
||||||
*/
|
|
||||||
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordPlayerSender>> OnlineSenders = new ConcurrentHashMap<>();
|
|
||||||
public static final ConcurrentHashMap<UUID, DiscordConnectedPlayer> LoggedInPlayers = new ConcurrentHashMap<>();
|
|
||||||
static @Nullable LastMsgData lastmsgdata;
|
|
||||||
static LongObjectHashMap<Message> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks
|
|
||||||
private static MinecraftChatModule module;
|
|
||||||
private static final HashMap<Class<? extends Event>, HashSet<String>> staticExcludedPlugins = new HashMap<>();
|
|
||||||
|
|
||||||
public static void updatePlayerList() {
|
|
||||||
val mod = getModule();
|
|
||||||
if (mod == null || !mod.showPlayerListOnDC.get()) return;
|
|
||||||
if (lastmsgdata != null)
|
|
||||||
updatePL(lastmsgdata);
|
|
||||||
MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean notEnabled() {
|
|
||||||
return (module == null || !module.disabling) && getModule() == null; //Allow using things while disabling the module
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MinecraftChatModule getModule() {
|
|
||||||
if (module == null || !module.isEnabled()) module = ComponentManager.getIfEnabled(MinecraftChatModule.class);
|
|
||||||
//If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updatePL(LastMsgData lmd) {
|
|
||||||
if (!(lmd.channel instanceof TextChannel)) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId(),
|
|
||||||
new Exception("The channel isn't a (guild) text channel."), getModule());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String topic = ((TextChannel) lmd.channel).getTopic().orElse("");
|
|
||||||
if (topic.length() == 0)
|
|
||||||
topic = ".\n----\nMinecraft chat\n----\n.";
|
|
||||||
String[] s = topic.split("\\n----\\n");
|
|
||||||
if (s.length < 3)
|
|
||||||
return;
|
|
||||||
String gid;
|
|
||||||
if (lmd instanceof MCChatCustom.CustomLMD)
|
|
||||||
gid = ((MCChatCustom.CustomLMD) lmd).groupID;
|
|
||||||
else //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most
|
|
||||||
gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE; // (Though it's a public chat then rn)
|
|
||||||
AtomicInteger C = new AtomicInteger();
|
|
||||||
s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream()
|
|
||||||
.filter(p -> (lmd.mcchannel == null
|
|
||||||
? gid.equals(buttondevteam.core.component.channel.Channel.GROUP_EVERYONE) //If null, allow if public (custom chats will have their channel stored anyway)
|
|
||||||
: gid.equals(lmd.mcchannel.getGroupID(p)))) //If they can see it
|
|
||||||
.filter(MCChatUtils::checkEssentials)
|
|
||||||
.filter(p -> C.incrementAndGet() > 0) //Always true
|
|
||||||
.map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", "));
|
|
||||||
s[0] = C + " player" + (C.get() != 1 ? "s" : "") + " online";
|
|
||||||
((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean checkEssentials(Player p) {
|
|
||||||
var ess = MainPlugin.ess;
|
|
||||||
if (ess == null) return true;
|
|
||||||
return !ess.getUser(p).isHidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends DiscordSenderBase> T addSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
|
|
||||||
User user, T sender) {
|
|
||||||
return addSender(senders, user.getId().asString(), sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends DiscordSenderBase> T addSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
|
|
||||||
String did, T sender) {
|
|
||||||
var map = senders.get(did);
|
|
||||||
if (map == null)
|
|
||||||
map = new ConcurrentHashMap<>();
|
|
||||||
map.put(sender.getChannel().getId(), sender);
|
|
||||||
senders.put(did, map);
|
|
||||||
return sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends DiscordSenderBase> T getSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
|
|
||||||
Snowflake channel, User user) {
|
|
||||||
var map = senders.get(user.getId().asString());
|
|
||||||
if (map != null)
|
|
||||||
return map.get(channel);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T extends DiscordSenderBase> T removeSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
|
|
||||||
Snowflake channel, User user) {
|
|
||||||
var map = senders.get(user.getId().asString());
|
|
||||||
if (map != null)
|
|
||||||
return map.remove(channel);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Mono<?> forPublicPrivateChat(Function<Mono<MessageChannel>, Mono<?>> action) {
|
|
||||||
if (notEnabled()) return Mono.empty();
|
|
||||||
var list = new ArrayList<Mono<?>>();
|
|
||||||
list.add(action.apply(module.chatChannelMono()));
|
|
||||||
for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
|
|
||||||
list.add(action.apply(Mono.just(data.channel)));
|
|
||||||
// lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat
|
|
||||||
return Mono.whenDelayError(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For custom and all MC chat
|
|
||||||
*
|
|
||||||
* @param action The action to act (cannot complete empty)
|
|
||||||
* @param toggle The toggle to check
|
|
||||||
* @param hookmsg Whether the message is also sent from the hook
|
|
||||||
*/
|
|
||||||
public static Mono<?> forCustomAndAllMCChat(Function<Mono<MessageChannel>, Mono<?>> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
|
|
||||||
if (notEnabled()) return Mono.empty();
|
|
||||||
var list = new ArrayList<Publisher<?>>();
|
|
||||||
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
|
|
||||||
list.add(forPublicPrivateChat(action));
|
|
||||||
final Function<MCChatCustom.CustomLMD, Publisher<?>> customLMDFunction = cc -> action.apply(Mono.just(cc.channel));
|
|
||||||
if (toggle == null)
|
|
||||||
MCChatCustom.lastmsgCustom.stream().map(customLMDFunction).forEach(list::add);
|
|
||||||
else
|
|
||||||
MCChatCustom.lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).map(customLMDFunction).forEach(list::add);
|
|
||||||
return Mono.whenDelayError(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
|
|
||||||
*
|
|
||||||
* @param action The action to do
|
|
||||||
* @param sender The sender to check perms of or null to send to all that has it toggled
|
|
||||||
* @param toggle The toggle to check or null to send to all allowed
|
|
||||||
*/
|
|
||||||
public static Mono<?> forAllowedCustomMCChat(Function<Mono<MessageChannel>, Mono<?>> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) {
|
|
||||||
if (notEnabled()) return Mono.empty();
|
|
||||||
Stream<Publisher<?>> st = MCChatCustom.lastmsgCustom.stream().filter(clmd -> {
|
|
||||||
//new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple
|
|
||||||
if (toggle != null && (clmd.toggles & toggle.flag) == 0)
|
|
||||||
return false; //If null then allow
|
|
||||||
if (sender == null)
|
|
||||||
return true;
|
|
||||||
return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender));
|
|
||||||
}).map(cc -> action.apply(Mono.just(cc.channel))); //TODO: Send error messages on channel connect
|
|
||||||
return Mono.whenDelayError(st::iterator); //Can't convert as an iterator or inside the stream, but I can convert it as a stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
|
|
||||||
*
|
|
||||||
* @param action The action to do
|
|
||||||
* @param sender The sender to check perms of or null to send to all that has it toggled
|
|
||||||
* @param toggle The toggle to check or null to send to all allowed
|
|
||||||
* @param hookmsg Whether the message is also sent from the hook
|
|
||||||
*/
|
|
||||||
public static Mono<?> forAllowedCustomAndAllMCChat(Function<Mono<MessageChannel>, Mono<?>> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
|
|
||||||
if (notEnabled()) return Mono.empty();
|
|
||||||
var cc = forAllowedCustomMCChat(action, sender, toggle);
|
|
||||||
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
|
|
||||||
return Mono.whenDelayError(forPublicPrivateChat(action), cc);
|
|
||||||
return Mono.whenDelayError(cc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Function<Mono<MessageChannel>, Mono<?>> send(String message) {
|
|
||||||
return ch -> ch.flatMap(mc -> {
|
|
||||||
resetLastMessage(mc);
|
|
||||||
return mc.createMessage(DPUtils.sanitizeString(message));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Mono<?> forAllowedMCChat(Function<Mono<MessageChannel>, Mono<?>> action, TBMCSystemChatEvent event) {
|
|
||||||
if (notEnabled()) return Mono.empty();
|
|
||||||
var list = new ArrayList<Mono<?>>();
|
|
||||||
if (event.getChannel().isGlobal())
|
|
||||||
list.add(action.apply(module.chatChannelMono()));
|
|
||||||
for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
|
|
||||||
if (event.shouldSendTo(getSender(data.channel.getId(), data.user)))
|
|
||||||
list.add(action.apply(Mono.just(data.channel))); //TODO: Only store ID?
|
|
||||||
MCChatCustom.lastmsgCustom.stream().filter(clmd -> {
|
|
||||||
if (!clmd.brtoggles.contains(event.getTarget()))
|
|
||||||
return false;
|
|
||||||
return event.shouldSendTo(clmd.dcp);
|
|
||||||
}).map(clmd -> action.apply(Mono.just(clmd.channel))).forEach(list::add);
|
|
||||||
return Mono.whenDelayError(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc.
|
|
||||||
*/
|
|
||||||
static DiscordSenderBase getSender(Snowflake channel, final User author) {
|
|
||||||
//noinspection OptionalGetWithoutIsPresent
|
|
||||||
return Stream.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239
|
|
||||||
() -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null
|
|
||||||
() -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it
|
|
||||||
() -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), //
|
|
||||||
() -> Optional.of(addSender(UnconnectedSenders, author,
|
|
||||||
new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the last message, so it will start a new one instead of appending to it.
|
|
||||||
* This is used when someone (even the bot) sends a message to the channel.
|
|
||||||
*
|
|
||||||
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
|
|
||||||
*/
|
|
||||||
public static void resetLastMessage(Channel channel) {
|
|
||||||
if (notEnabled()) return;
|
|
||||||
if (channel.getId().asLong() == module.chatChannel.get().asLong()) {
|
|
||||||
(lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null)
|
|
||||||
: lastmsgdata).message = null;
|
|
||||||
return;
|
|
||||||
} // Don't set the whole object to null, the player and channel information should be preserved
|
|
||||||
for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) {
|
|
||||||
if (data.channel.getId().asLong() == channel.getId().asLong()) {
|
|
||||||
data.message = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//If it gets here, it's sending a message to a non-chat channel
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addStaticExcludedPlugin(Class<? extends Event> event, String plugin) {
|
|
||||||
staticExcludedPlugins.compute(event, (e, hs) -> hs == null
|
|
||||||
? Sets.newHashSet(plugin)
|
|
||||||
: (hs.add(plugin) ? hs : hs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void callEventExcludingSome(Event event) {
|
|
||||||
if (notEnabled()) return;
|
|
||||||
val second = staticExcludedPlugins.get(event.getClass());
|
|
||||||
String[] first = module.excludedPlugins.get();
|
|
||||||
String[] both = second == null ? first
|
|
||||||
: Arrays.copyOf(first, first.length + second.size());
|
|
||||||
int i = first.length;
|
|
||||||
if (second != null)
|
|
||||||
for (String plugin : second)
|
|
||||||
both[i++] = plugin;
|
|
||||||
callEventExcluding(event, false, both);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls an event with the given details.
|
|
||||||
* <p>
|
|
||||||
* This method only synchronizes when the event is not asynchronous.
|
|
||||||
*
|
|
||||||
* @param event Event details
|
|
||||||
* @param only Flips the operation and <b>includes</b> the listed plugins
|
|
||||||
* @param plugins The plugins to exclude. Not case sensitive.
|
|
||||||
*/
|
|
||||||
public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit
|
|
||||||
if (event.isAsynchronous()) {
|
|
||||||
if (Thread.holdsLock(Bukkit.getPluginManager())) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.");
|
|
||||||
}
|
|
||||||
if (Bukkit.getServer().isPrimaryThread()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
event.getEventName() + " cannot be triggered asynchronously from primary server thread.");
|
|
||||||
}
|
|
||||||
fireEventExcluding(event, only, plugins);
|
|
||||||
} else {
|
|
||||||
synchronized (Bukkit.getPluginManager()) {
|
|
||||||
fireEventExcluding(event, only, plugins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void fireEventExcluding(Event event, boolean only, String... plugins) {
|
|
||||||
HandlerList handlers = event.getHandlers(); // Code taken from SimplePluginManager in Spigot-API
|
|
||||||
RegisteredListener[] listeners = handlers.getRegisteredListeners();
|
|
||||||
val server = Bukkit.getServer();
|
|
||||||
|
|
||||||
for (RegisteredListener registration : listeners) {
|
|
||||||
if (!registration.getPlugin().isEnabled()
|
|
||||||
|| Arrays.stream(plugins).anyMatch(p -> only ^ p.equalsIgnoreCase(registration.getPlugin().getName())))
|
|
||||||
continue; // Modified to exclude plugins
|
|
||||||
|
|
||||||
try {
|
|
||||||
registration.callEvent(event);
|
|
||||||
} catch (AuthorNagException ex) {
|
|
||||||
Plugin plugin = registration.getPlugin();
|
|
||||||
|
|
||||||
if (plugin.isNaggable()) {
|
|
||||||
plugin.setNaggable(false);
|
|
||||||
|
|
||||||
server.getLogger().log(Level.SEVERE,
|
|
||||||
String.format("Nag author(s): '%s' of '%s' about the following: %s",
|
|
||||||
plugin.getDescription().getAuthors(), plugin.getDescription().getFullName(),
|
|
||||||
ex.getMessage()));
|
|
||||||
}
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
server.getLogger().log(Level.SEVERE, "Could not pass event " + event.getEventName() + " to "
|
|
||||||
+ registration.getPlugin().getDescription().getFullName(), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call it from an async thread.
|
|
||||||
*/
|
|
||||||
public static void callLoginEvents(DiscordConnectedPlayer dcp) {
|
|
||||||
Consumer<Supplier<String>> loginFail = kickMsg -> {
|
|
||||||
dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg.get());
|
|
||||||
MCChatPrivate.privateMCChat(dcp.getChannel(), false, dcp.getUser(), dcp.getChromaUser());
|
|
||||||
}; //Probably also happens if the user is banned or so
|
|
||||||
val event = new AsyncPlayerPreLoginEvent(dcp.getName(), InetAddress.getLoopbackAddress(), dcp.getUniqueId());
|
|
||||||
callEventExcludingSome(event);
|
|
||||||
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
|
|
||||||
loginFail.accept(event::getKickMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> {
|
|
||||||
val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress());
|
|
||||||
callEventExcludingSome(ev);
|
|
||||||
if (ev.getResult() != PlayerLoginEvent.Result.ALLOWED) {
|
|
||||||
loginFail.accept(ev::getKickMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callEventExcludingSome(new PlayerJoinEvent(dcp, ""));
|
|
||||||
dcp.setLoggedIn(true);
|
|
||||||
if (module != null) {
|
|
||||||
if (module.serverWatcher != null)
|
|
||||||
module.serverWatcher.fakePlayers.add(dcp);
|
|
||||||
module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged in from Discord");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only calls the events if the player is actually logged in
|
|
||||||
*
|
|
||||||
* @param dcp The player
|
|
||||||
* @param needsSync Whether we're in an async thread
|
|
||||||
*/
|
|
||||||
public static void callLogoutEvent(DiscordConnectedPlayer dcp, boolean needsSync) {
|
|
||||||
if (!dcp.isLoggedIn()) return;
|
|
||||||
val event = new PlayerQuitEvent(dcp, "");
|
|
||||||
if (needsSync) callEventSync(event);
|
|
||||||
else callEventExcludingSome(event);
|
|
||||||
dcp.setLoggedIn(false);
|
|
||||||
if (module != null) {
|
|
||||||
module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord");
|
|
||||||
if (module.serverWatcher != null)
|
|
||||||
module.serverWatcher.fakePlayers.remove(dcp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void callEventSync(Event event) {
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public static class LastMsgData {
|
|
||||||
public Message message;
|
|
||||||
public long time;
|
|
||||||
public String content;
|
|
||||||
public final MessageChannel channel;
|
|
||||||
public buttondevteam.core.component.channel.Channel mcchannel;
|
|
||||||
public final User user;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.*;
|
|
||||||
import buttondevteam.lib.TBMCSystemChatEvent;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayerBase;
|
|
||||||
import buttondevteam.lib.player.TBMCYEEHAWEvent;
|
|
||||||
import com.earth2me.essentials.CommandSource;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import lombok.val;
|
|
||||||
import net.ess3.api.events.AfkStatusChangeEvent;
|
|
||||||
import net.ess3.api.events.MuteStatusChangeEvent;
|
|
||||||
import net.ess3.api.events.NickChangeEvent;
|
|
||||||
import net.ess3.api.events.VanishStatusChangeEvent;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.EventPriority;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
|
||||||
import org.bukkit.event.player.*;
|
|
||||||
import org.bukkit.event.player.PlayerLoginEvent.Result;
|
|
||||||
import org.bukkit.event.server.BroadcastMessageEvent;
|
|
||||||
import org.bukkit.event.server.TabCompleteEvent;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class MCListener implements Listener {
|
|
||||||
private final MinecraftChatModule module;
|
|
||||||
private final ConfigData<Mono<Role>> muteRole;
|
|
||||||
|
|
||||||
public MCListener(MinecraftChatModule module) {
|
|
||||||
this.module = module;
|
|
||||||
muteRole = DPUtils.roleData(module.getConfig(), "muteRole", "Muted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
|
||||||
public void onPlayerLogin(PlayerLoginEvent e) {
|
|
||||||
if (e.getResult() != Result.ALLOWED)
|
|
||||||
return;
|
|
||||||
if (e.getPlayer() instanceof DiscordConnectedPlayer)
|
|
||||||
return;
|
|
||||||
var dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId());
|
|
||||||
if (dcp != null)
|
|
||||||
MCChatUtils.callLogoutEvent(dcp, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onPlayerJoin(PlayerJoinEvent e) {
|
|
||||||
if (e.getPlayer() instanceof DiscordConnectedPlayer)
|
|
||||||
return; // Don't show the joined message for the fake player
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
|
|
||||||
final Player p = e.getPlayer();
|
|
||||||
DiscordPlayer dp = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class).getAs(DiscordPlayer.class);
|
|
||||||
if (dp != null) {
|
|
||||||
DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).flatMap(user -> user.getPrivateChannel().flatMap(chan -> module.chatChannelMono().flatMap(cc -> {
|
|
||||||
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
|
|
||||||
DiscordPlayerSender.create(user, chan, p, module));
|
|
||||||
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
|
|
||||||
DiscordPlayerSender.create(user, cc, p, module)); //Stored per-channel
|
|
||||||
return Mono.empty();
|
|
||||||
}))).subscribe();
|
|
||||||
}
|
|
||||||
final String message = e.getJoinMessage();
|
|
||||||
if (message != null && message.trim().length() > 0)
|
|
||||||
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe();
|
|
||||||
ChromaBot.getInstance().updatePlayerList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onPlayerLeave(PlayerQuitEvent e) {
|
|
||||||
if (e.getPlayer() instanceof DiscordConnectedPlayer)
|
|
||||||
return; // Only care about real users
|
|
||||||
MCChatUtils.OnlineSenders.entrySet()
|
|
||||||
.removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId())));
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin,
|
|
||||||
() -> Optional.ofNullable(MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId())).ifPresent(MCChatUtils::callLoginEvents));
|
|
||||||
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
|
|
||||||
ChromaBot.getInstance()::updatePlayerList, 5);
|
|
||||||
final String message = e.getQuitMessage();
|
|
||||||
if (message != null && message.trim().length() > 0)
|
|
||||||
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
|
||||||
public void onPlayerKick(PlayerKickEvent e) {
|
|
||||||
/*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting")
|
|
||||||
&& !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special
|
|
||||||
MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.LOW)
|
|
||||||
public void onPlayerDeath(PlayerDeathEvent e) {
|
|
||||||
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage()), e.getEntity(), ChannelconBroadcast.DEATH, true).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onPlayerAFK(AfkStatusChangeEvent e) {
|
|
||||||
final Player base = e.getAffected().getBase();
|
|
||||||
if (e.isCancelled() || !base.isOnline())
|
|
||||||
return;
|
|
||||||
final String msg = base.getDisplayName()
|
|
||||||
+ " is " + (e.getValue() ? "now" : "no longer") + " AFK.";
|
|
||||||
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onPlayerMute(MuteStatusChangeEvent e) {
|
|
||||||
final Mono<Role> role = muteRole.get();
|
|
||||||
if (role == null) return;
|
|
||||||
final CommandSource source = e.getAffected().getSource();
|
|
||||||
if (!source.isPlayer())
|
|
||||||
return;
|
|
||||||
final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class)
|
|
||||||
.getAs(DiscordPlayer.class);
|
|
||||||
if (p == null) return;
|
|
||||||
DPUtils.ignoreError(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID()))
|
|
||||||
.flatMap(user -> user.asMember(DiscordPlugin.mainServer.getId()))
|
|
||||||
.flatMap(user -> role.flatMap(r -> {
|
|
||||||
if (e.getValue())
|
|
||||||
user.addRole(r.getId());
|
|
||||||
else
|
|
||||||
user.removeRole(r.getId());
|
|
||||||
val modlog = module.modlogChannel.get();
|
|
||||||
String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getUsername() + "#" + user.getDiscriminator();
|
|
||||||
module.log(msg);
|
|
||||||
if (modlog != null)
|
|
||||||
return modlog.flatMap(ch -> ch.createMessage(msg));
|
|
||||||
return Mono.empty();
|
|
||||||
}))).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onChatSystemMessage(TBMCSystemChatEvent event) {
|
|
||||||
MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage()), event).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onBroadcastMessage(BroadcastMessageEvent event) {
|
|
||||||
MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage()), ChannelconBroadcast.BROADCAST, false).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onYEEHAW(TBMCYEEHAWEvent event) { //TODO: Inherit from the chat event base to have channel support
|
|
||||||
String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName()
|
|
||||||
: event.getSender().getName();
|
|
||||||
//Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO
|
|
||||||
DiscordPlugin.mainServer.getEmojis().filter(e -> "YEEHAW".equals(e.getName()))
|
|
||||||
.take(1).singleOrEmpty().map(Optional::of).defaultIfEmpty(Optional.empty()).flatMap(yeehaw ->
|
|
||||||
MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name + (yeehaw.map(guildEmoji -> " <:YEEHAW:" + guildEmoji.getId().asString() + ">s").orElse(" YEEHAWs"))))).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onNickChange(NickChangeEvent event) {
|
|
||||||
MCChatUtils.updatePlayerList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onTabComplete(TabCompleteEvent event) {
|
|
||||||
int i = event.getBuffer().lastIndexOf(' ');
|
|
||||||
String t = event.getBuffer().substring(i + 1); //0 if not found
|
|
||||||
if (!t.startsWith("@"))
|
|
||||||
return;
|
|
||||||
String token = t.substring(1);
|
|
||||||
val x = DiscordPlugin.mainServer.getMembers()
|
|
||||||
.flatMap(m -> Flux.just(m.getUsername(), m.getNickname().orElse("")))
|
|
||||||
.filter(s -> s.startsWith(token))
|
|
||||||
.map(s -> "@" + s)
|
|
||||||
.doOnNext(event.getCompletions()::add).blockLast();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onCommandSend(PlayerCommandSendEvent event) {
|
|
||||||
event.getCommands().add("g");
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onVanish(VanishStatusChangeEvent event) {
|
|
||||||
if (event.isCancelled()) return;
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, MCChatUtils::updatePlayerList);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,261 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mcchat;
|
|
||||||
|
|
||||||
import buttondevteam.core.component.channel.Channel;
|
|
||||||
import buttondevteam.discordplugin.ChannelconBroadcast;
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordConnectedPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.ServerWatcher;
|
|
||||||
import buttondevteam.discordplugin.playerfaker.perm.LPInjector;
|
|
||||||
import buttondevteam.discordplugin.util.DPState;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.TBMCSystemChatEvent;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ConfigData;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import discord4j.common.util.Snowflake;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.rest.util.Color;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM.
|
|
||||||
*/
|
|
||||||
public class MinecraftChatModule extends Component<DiscordPlugin> {
|
|
||||||
public static DPState state = DPState.RUNNING;
|
|
||||||
private @Getter MCChatListener listener;
|
|
||||||
ServerWatcher serverWatcher;
|
|
||||||
private LPInjector lpInjector;
|
|
||||||
boolean disabling = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command!
|
|
||||||
*/
|
|
||||||
public ConfigData<ArrayList<String>> whitelistedCommands() {
|
|
||||||
return getConfig().getData("whitelistedCommands", () -> Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki",
|
|
||||||
"yeehaw", "lenny", "rp", "plugins"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel to use as the public Minecraft chat - everything public gets broadcasted here
|
|
||||||
*/
|
|
||||||
public ReadOnlyConfigData<Snowflake> chatChannel = DPUtils.snowflakeData(getConfig(), "chatChannel", 0L);
|
|
||||||
|
|
||||||
public Mono<MessageChannel> chatChannelMono() {
|
|
||||||
return DPUtils.getMessageChannel(chatChannel.getPath(), chatChannel.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute
|
|
||||||
*/
|
|
||||||
public ReadOnlyConfigData<Mono<MessageChannel>> modlogChannel = DPUtils.channelData(getConfig(), "modlogChannel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here
|
|
||||||
*/
|
|
||||||
public ConfigData<String[]> excludedPlugins = getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands.
|
|
||||||
* They can then use commands like /tpahere to teleport others to that place.<br />
|
|
||||||
* If this is off, then teleporting will have no effect.
|
|
||||||
*/
|
|
||||||
public ConfigData<Boolean> allowFakePlayerTeleports = getConfig().getData("allowFakePlayerTeleports", false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is on, each chat channel will have a player list in their description.
|
|
||||||
* It only gets added if there's no description yet or there are (at least) two lines of "----" following each other.
|
|
||||||
* Note that it will replace <b>everything</b> above the first and below the last "----" but it will only detect exactly four dashes.
|
|
||||||
* So if you want to use dashes for something else in the description, make sure it's either less or more dashes in one line.
|
|
||||||
*/
|
|
||||||
public ConfigData<Boolean> showPlayerListOnDC = getConfig().getData("showPlayerListOnDC", true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This setting controls whether custom chat connections can be <i>created</i> (existing connections will always work).
|
|
||||||
* Custom chat connections can be created using the channelcon command and they allow players to display town chat in a Discord channel for example.
|
|
||||||
* See the channelcon command for more details.
|
|
||||||
*/
|
|
||||||
public ConfigData<Boolean> allowCustomChat = getConfig().getData("allowCustomChat", true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This setting allows you to control if players can DM the bot to log on the server from Discord.
|
|
||||||
* This allows them to both chat and perform any command they can in-game.
|
|
||||||
*/
|
|
||||||
public ConfigData<Boolean> allowPrivateChat = getConfig().getData("allowPrivateChat", true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID.
|
|
||||||
*/
|
|
||||||
public ConfigData<String> profileURL = getConfig().getData("profileURL", "");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables support for running vanilla commands through Discord, if you ever need it.
|
|
||||||
*/
|
|
||||||
public ConfigData<Boolean> enableVanillaCommands = getConfig().getData("enableVanillaCommands", true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether players logged on from Discord (mcchat command) should be recognised by other plugins. Some plugins might break if it's turned off.
|
|
||||||
* But it's really hacky.
|
|
||||||
*/
|
|
||||||
private final ConfigData<Boolean> addFakePlayersToBukkit = getConfig().getData("addFakePlayersToBukkit", false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set by the component to report crashes.
|
|
||||||
*/
|
|
||||||
private final ConfigData<Boolean> serverUp = getConfig().getData("serverUp", false);
|
|
||||||
|
|
||||||
private final MCChatCommand mcChatCommand = new MCChatCommand(this);
|
|
||||||
private final ChannelconCommand channelconCommand = new ChannelconCommand(this);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
if (DPUtils.disableIfConfigErrorRes(this, chatChannel, chatChannelMono()))
|
|
||||||
return;
|
|
||||||
listener = new MCChatListener(this);
|
|
||||||
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin());
|
|
||||||
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled
|
|
||||||
getPlugin().getManager().registerCommand(mcChatCommand);
|
|
||||||
getPlugin().getManager().registerCommand(channelconCommand);
|
|
||||||
|
|
||||||
val chcons = getConfig().getConfig().getConfigurationSection("chcons");
|
|
||||||
if (chcons == null) //Fallback to old place
|
|
||||||
getConfig().getConfig().getRoot().getConfigurationSection("chcons");
|
|
||||||
if (chcons != null) {
|
|
||||||
val chconkeys = chcons.getKeys(false);
|
|
||||||
for (val chconkey : chconkeys) {
|
|
||||||
val chcon = chcons.getConfigurationSection(chconkey);
|
|
||||||
val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny();
|
|
||||||
val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block();
|
|
||||||
val did = chcon.getLong("did");
|
|
||||||
val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block();
|
|
||||||
val groupid = chcon.getString("groupid");
|
|
||||||
val toggles = chcon.getInt("toggles");
|
|
||||||
val brtoggles = chcon.getStringList("brtoggles");
|
|
||||||
if (!mcch.isPresent() || ch == null || user == null || groupid == null)
|
|
||||||
continue;
|
|
||||||
Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase)
|
|
||||||
val dcp = DiscordConnectedPlayer.create(user, (MessageChannel) ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this);
|
|
||||||
MCChatCustom.addCustomChat((MessageChannel) ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (lpInjector == null)
|
|
||||||
lpInjector = new LPInjector(DiscordPlugin.plugin);
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e, this);
|
|
||||||
} catch (NoClassDefFoundError e) {
|
|
||||||
log("No LuckPerms, not injecting");
|
|
||||||
//e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addFakePlayersToBukkit.get()) {
|
|
||||||
try {
|
|
||||||
serverWatcher = new ServerWatcher();
|
|
||||||
serverWatcher.enableDisable(true);
|
|
||||||
log("Finished hooking into the server");
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to hack the server (object)! Disable addFakePlayersToBukkit in the config.", e, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == DPState.RESTARTING_PLUGIN) { //These will only execute if the chat is enabled
|
|
||||||
sendStateMessage(Color.CYAN, "Discord plugin restarted - chat connected."); //Really important to note the chat, hmm
|
|
||||||
state = DPState.RUNNING;
|
|
||||||
} else if (state == DPState.DISABLED_MCCHAT) {
|
|
||||||
sendStateMessage(Color.CYAN, "Minecraft chat enabled - chat connected.");
|
|
||||||
state = DPState.RUNNING;
|
|
||||||
} else if (serverUp.get()) {
|
|
||||||
sendStateMessage(Color.YELLOW, "Server started after a crash - chat connected.");
|
|
||||||
val thr = new Throwable("The server shut down unexpectedly. See the log of the previous run for more details.");
|
|
||||||
thr.setStackTrace(new StackTraceElement[0]);
|
|
||||||
TBMCCoreAPI.SendException("The server crashed!", thr, this);
|
|
||||||
} else
|
|
||||||
sendStateMessage(Color.GREEN, "Server started - chat connected.");
|
|
||||||
serverUp.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
disabling = true;
|
|
||||||
if (state == DPState.RESTARTING_PLUGIN) //These will only execute if the chat is enabled
|
|
||||||
sendStateMessage(Color.ORANGE, "Discord plugin restarting");
|
|
||||||
else if (state == DPState.RUNNING) {
|
|
||||||
sendStateMessage(Color.ORANGE, "Minecraft chat disabled");
|
|
||||||
state = DPState.DISABLED_MCCHAT;
|
|
||||||
} else {
|
|
||||||
String kickmsg = Bukkit.getOnlinePlayers().size() > 0
|
|
||||||
? (DPUtils
|
|
||||||
.sanitizeString(Bukkit.getOnlinePlayers().stream()
|
|
||||||
.map(Player::getDisplayName).collect(Collectors.joining(", ")))
|
|
||||||
+ (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ")
|
|
||||||
+ "thrown out") //TODO: Make configurable
|
|
||||||
: "";
|
|
||||||
if (state == DPState.RESTARTING_SERVER)
|
|
||||||
sendStateMessage(Color.ORANGE, "Server restarting", kickmsg);
|
|
||||||
else if (state == DPState.STOPPING_SERVER)
|
|
||||||
sendStateMessage(Color.RED, "Server stopping", kickmsg);
|
|
||||||
else
|
|
||||||
sendStateMessage(Color.GRAY, "Unknown state, please report.");
|
|
||||||
} //If 'restart' is disabled then this isn't shown even if joinleave is enabled
|
|
||||||
|
|
||||||
serverUp.set(false); //Disable even if just the component is disabled because that way it won't falsely report crashes
|
|
||||||
|
|
||||||
try { //If it's not enabled it won't do anything
|
|
||||||
if (serverWatcher != null) {
|
|
||||||
serverWatcher.enableDisable(false);
|
|
||||||
log("Finished unhooking the server");
|
|
||||||
}
|
|
||||||
} catch (
|
|
||||||
Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Failed to restore the server object!", e, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
val chcons = MCChatCustom.getCustomChats();
|
|
||||||
val chconsc = getConfig().getConfig().createSection("chcons");
|
|
||||||
for (
|
|
||||||
val chcon : chcons) {
|
|
||||||
val chconc = chconsc.createSection(chcon.channel.getId().asString());
|
|
||||||
chconc.set("mcchid", chcon.mcchannel.ID);
|
|
||||||
chconc.set("chid", chcon.channel.getId().asLong());
|
|
||||||
chconc.set("did", chcon.user.getId().asLong());
|
|
||||||
chconc.set("mcuid", chcon.dcp.getUniqueId().toString());
|
|
||||||
chconc.set("mcname", chcon.dcp.getName());
|
|
||||||
chconc.set("groupid", chcon.groupID);
|
|
||||||
chconc.set("toggles", chcon.toggles);
|
|
||||||
chconc.set("brtoggles", chcon.brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::getName).collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
if (listener != null) //Can be null if disabled because of a config error
|
|
||||||
listener.stop(true);
|
|
||||||
getPlugin().getManager().unregisterCommand(mcChatCommand);
|
|
||||||
getPlugin().getManager().unregisterCommand(channelconCommand);
|
|
||||||
disabling = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It will block to make sure all messages are sent
|
|
||||||
*/
|
|
||||||
private void sendStateMessage(Color color, String message) {
|
|
||||||
MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(color)
|
|
||||||
.setTitle(message))), ChannelconBroadcast.RESTART, false).block();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It will block to make sure all messages are sent
|
|
||||||
*/
|
|
||||||
private void sendStateMessage(Color color, String message, String extra) {
|
|
||||||
MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(color)
|
|
||||||
.setTitle(message).setDescription(extra)).onErrorResume(t -> Mono.empty())), ChannelconBroadcast.RESTART, false).block();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
package buttondevteam.discordplugin.mccommands;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.discordplugin.commands.ConnectCommand;
|
|
||||||
import buttondevteam.discordplugin.commands.VersionCommand;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.discordplugin.util.DPState;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import buttondevteam.lib.chat.ICommand2MC;
|
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayer;
|
|
||||||
import buttondevteam.lib.player.TBMCPlayerBase;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
@CommandClass(path = "discord", helpText = {
|
|
||||||
"Discord",
|
|
||||||
"This command allows performing Discord-related actions."
|
|
||||||
})
|
|
||||||
public class DiscordMCCommand extends ICommand2MC {
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean accept(Player player) {
|
|
||||||
if (checkSafeMode(player)) return true;
|
|
||||||
String did = ConnectCommand.WaitingToConnect.get(player.getName());
|
|
||||||
if (did == null) {
|
|
||||||
player.sendMessage("§cYou don't have a pending connection to Discord.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
DiscordPlayer dp = ChromaGamerBase.getUser(did, DiscordPlayer.class);
|
|
||||||
TBMCPlayer mcp = TBMCPlayerBase.getPlayer(player.getUniqueId(), TBMCPlayer.class);
|
|
||||||
dp.connectWith(mcp);
|
|
||||||
ConnectCommand.WaitingToConnect.remove(player.getName());
|
|
||||||
MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed
|
|
||||||
player.sendMessage("§bAccounts connected.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public boolean decline(Player player) {
|
|
||||||
if (checkSafeMode(player)) return true;
|
|
||||||
String did = ConnectCommand.WaitingToConnect.remove(player.getName());
|
|
||||||
if (did == null) {
|
|
||||||
player.sendMessage("§cYou don't have a pending connection to Discord.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
player.sendMessage("§bPending connection declined.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = {
|
|
||||||
"Reload Discord plugin",
|
|
||||||
"Reloads the config. To apply some changes, you may need to also run /discord restart."
|
|
||||||
})
|
|
||||||
public void reload(CommandSender sender) {
|
|
||||||
if (DiscordPlugin.plugin.tryReloadConfig())
|
|
||||||
sender.sendMessage("§bConfig reloaded.");
|
|
||||||
else
|
|
||||||
sender.sendMessage("§cFailed to reload config.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = {
|
|
||||||
"Restart the plugin", //
|
|
||||||
"This command disables and then enables the plugin." //
|
|
||||||
})
|
|
||||||
public void restart(CommandSender sender) {
|
|
||||||
Runnable task = () -> {
|
|
||||||
if (!DiscordPlugin.plugin.tryReloadConfig()) {
|
|
||||||
sender.sendMessage("§cFailed to reload config so not restarting. Check the console.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MinecraftChatModule.state = DPState.RESTARTING_PLUGIN; //Reset in MinecraftChatModule
|
|
||||||
sender.sendMessage("§bDisabling DiscordPlugin...");
|
|
||||||
Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin);
|
|
||||||
if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors
|
|
||||||
sender.sendMessage("§bEnabling DiscordPlugin...");
|
|
||||||
Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin);
|
|
||||||
if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors
|
|
||||||
sender.sendMessage("§bRestart finished!");
|
|
||||||
};
|
|
||||||
if (!Bukkit.getName().equals("Paper")) {
|
|
||||||
getPlugin().getLogger().warning("Async plugin events are not supported by the server, running on main thread");
|
|
||||||
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, task);
|
|
||||||
} else
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, task);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(helpText = {
|
|
||||||
"Version command",
|
|
||||||
"Prints the plugin version"
|
|
||||||
})
|
|
||||||
public void version(CommandSender sender) {
|
|
||||||
sender.sendMessage(VersionCommand.getVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(helpText = {
|
|
||||||
"Invite",
|
|
||||||
"Shows an invite link to the server"
|
|
||||||
})
|
|
||||||
public void invite(CommandSender sender) {
|
|
||||||
if (checkSafeMode(sender)) return;
|
|
||||||
String invi = DiscordPlugin.plugin.inviteLink.get();
|
|
||||||
if (invi.length() > 0) {
|
|
||||||
sender.sendMessage("§bInvite link: " + invi);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DiscordPlugin.mainServer.getInvites().limitRequest(1)
|
|
||||||
.switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("§cNo invites found for the server.")))
|
|
||||||
.subscribe(inv -> sender.sendMessage("§bInvite link: https://discord.gg/" + inv.getCode()),
|
|
||||||
e -> sender.sendMessage("§cThe invite link is not set and the bot has no permission to get it."));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getHelpText(Method method, Command2.Subcommand ann) {
|
|
||||||
switch (method.getName()) {
|
|
||||||
case "accept":
|
|
||||||
return new String[]{ //
|
|
||||||
"Accept Discord connection", //
|
|
||||||
"Accept a pending connection between your Discord and Minecraft account.", //
|
|
||||||
"To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention() + " channel on Discord", //
|
|
||||||
};
|
|
||||||
case "decline":
|
|
||||||
return new String[]{ //
|
|
||||||
"Decline Discord connection", //
|
|
||||||
"Decline a pending connection between your Discord and Minecraft account.", //
|
|
||||||
"To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention() + " channel on Discord", //
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return super.getHelpText(method, ann);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkSafeMode(CommandSender sender) {
|
|
||||||
if (DiscordPlugin.SafeMode) {
|
|
||||||
sender.sendMessage("§cThe plugin isn't initialized. Check console for details.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.Delegate;
|
|
||||||
import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker;
|
|
||||||
import org.mockito.plugins.MockMaker;
|
|
||||||
|
|
||||||
public class DelegatingMockMaker implements MockMaker {
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Delegate
|
|
||||||
private MockMaker mockMaker = new SubclassByteBuddyMockMaker();
|
|
||||||
@Getter
|
|
||||||
private static DelegatingMockMaker instance;
|
|
||||||
|
|
||||||
public DelegatingMockMaker() {
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
|
||||||
import com.destroystokyo.paper.profile.CraftPlayerProfile;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Server;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker;
|
|
||||||
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class ServerWatcher {
|
|
||||||
private List<Player> playerList;
|
|
||||||
public final List<Player> fakePlayers = new ArrayList<>();
|
|
||||||
private Server origServer;
|
|
||||||
|
|
||||||
@IgnoreForBinding
|
|
||||||
public void enableDisable(boolean enable) throws Exception {
|
|
||||||
var serverField = Bukkit.class.getDeclaredField("server");
|
|
||||||
serverField.setAccessible(true);
|
|
||||||
if (enable) {
|
|
||||||
var serverClass = Bukkit.getServer().getClass();
|
|
||||||
var originalServer = serverField.get(null);
|
|
||||||
DelegatingMockMaker.getInstance().setMockMaker(new InlineByteBuddyMockMaker());
|
|
||||||
var settings = Mockito.withSettings().stubOnly()
|
|
||||||
.defaultAnswer(invocation -> {
|
|
||||||
var method = invocation.getMethod();
|
|
||||||
int pc = method.getParameterCount();
|
|
||||||
Player player = null;
|
|
||||||
switch (method.getName()) {
|
|
||||||
case "getPlayer":
|
|
||||||
if (pc == 1 && method.getParameterTypes()[0] == UUID.class)
|
|
||||||
player = MCChatUtils.LoggedInPlayers.get(invocation.<UUID>getArgument(0));
|
|
||||||
break;
|
|
||||||
case "getPlayerExact":
|
|
||||||
if (pc == 1) {
|
|
||||||
final String argument = invocation.getArgument(0);
|
|
||||||
player = MCChatUtils.LoggedInPlayers.values().stream()
|
|
||||||
.filter(dcp -> dcp.getName().equalsIgnoreCase(argument)).findAny().orElse(null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
/*case "getOnlinePlayers":
|
|
||||||
if (playerList == null) {
|
|
||||||
@SuppressWarnings("unchecked") var list = (List<Player>) method.invoke(origServer, invocation.getArguments());
|
|
||||||
playerList = new AppendListView<>(list, fakePlayers);
|
|
||||||
} - Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.
|
|
||||||
return playerList;*/
|
|
||||||
case "createProfile": //Paper's method, casts the player to a CraftPlayer
|
|
||||||
if (pc == 2) {
|
|
||||||
UUID uuid = invocation.getArgument(0);
|
|
||||||
String name = invocation.getArgument(1);
|
|
||||||
player = uuid != null ? MCChatUtils.LoggedInPlayers.get(uuid) : null;
|
|
||||||
if (player == null && name != null)
|
|
||||||
player = MCChatUtils.LoggedInPlayers.values().stream()
|
|
||||||
.filter(dcp -> dcp.getName().equalsIgnoreCase(name)).findAny().orElse(null);
|
|
||||||
if (player != null)
|
|
||||||
return new CraftPlayerProfile(player.getUniqueId(), player.getName());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (player != null)
|
|
||||||
return player;
|
|
||||||
return method.invoke(origServer, invocation.getArguments());
|
|
||||||
});
|
|
||||||
//var mock = mockMaker.createMock(settings, MockHandlerFactory.createMockHandler(settings));
|
|
||||||
//thread.setContextClassLoader(cl);
|
|
||||||
var mock = Mockito.mock(serverClass, settings);
|
|
||||||
for (var field : serverClass.getFields()) //Copy public fields, private fields aren't accessible directly anyways
|
|
||||||
if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers()))
|
|
||||||
field.set(mock, field.get(originalServer));
|
|
||||||
serverField.set(null, mock);
|
|
||||||
origServer = (Server) originalServer;
|
|
||||||
} else if (origServer != null)
|
|
||||||
serverField.set(null, origServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public static class AppendListView<T> extends AbstractSequentialList<T> {
|
|
||||||
private final List<T> originalList;
|
|
||||||
private final List<T> additionalList;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListIterator<T> listIterator(int i) {
|
|
||||||
int os = originalList.size();
|
|
||||||
return i < os ? originalList.listIterator(i) : additionalList.listIterator(i - os);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return originalList.size() + additionalList.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.discordplugin.IMCPlayer;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class VCMDWrapper {
|
|
||||||
@Getter //Needed to mock the player
|
|
||||||
@Nullable
|
|
||||||
private final Object listener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will only send raw vanilla messages to the sender in plain text.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
*/
|
|
||||||
public static <T extends DiscordSenderBase & IMCPlayer<T>> Object createListener(T player, MinecraftChatModule module) {
|
|
||||||
return createListener(player, null, module);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
* @param bukkitplayer The Bukkit player to send the raw message to
|
|
||||||
* @param module The Minecraft chat module
|
|
||||||
*/
|
|
||||||
public static <T extends DiscordSenderBase & IMCPlayer<T>> Object createListener(T player, Player bukkitplayer, MinecraftChatModule module) {
|
|
||||||
try {
|
|
||||||
Object ret;
|
|
||||||
String mcpackage = Bukkit.getServer().getClass().getPackage().getName();
|
|
||||||
if (mcpackage.contains("1_12"))
|
|
||||||
ret = new VanillaCommandListener<>(player, bukkitplayer);
|
|
||||||
else if (mcpackage.contains("1_14"))
|
|
||||||
ret = new VanillaCommandListener14<>(player, bukkitplayer);
|
|
||||||
else if (mcpackage.contains("1_15") || mcpackage.contains("1_16"))
|
|
||||||
ret = VanillaCommandListener15.create(player, bukkitplayer); //bukkitplayer may be null but that's fine
|
|
||||||
else
|
|
||||||
ret = null;
|
|
||||||
if (ret == null)
|
|
||||||
compatWarning(module);
|
|
||||||
return ret;
|
|
||||||
} catch (NoClassDefFoundError | Exception e) {
|
|
||||||
compatWarning(module);
|
|
||||||
TBMCCoreAPI.SendException("Failed to create vanilla command listener", e, module);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void compatWarning(MinecraftChatModule module) {
|
|
||||||
module.logWarn("Vanilla commands won't be available from Discord due to a compatibility error. Disable vanilla command support to remove this message.");
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean compatResponse(DiscordSenderBase dsender) {
|
|
||||||
dsender.sendMessage("Vanilla commands are not supported on this Minecraft version.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.discordplugin.IMCPlayer;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
import net.minecraft.server.v1_12_R1.*;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.craftbukkit.v1_12_R1.CraftServer;
|
|
||||||
import org.bukkit.craftbukkit.v1_12_R1.CraftWorld;
|
|
||||||
import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper;
|
|
||||||
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class VanillaCommandListener<T extends DiscordSenderBase & IMCPlayer<T>> implements ICommandListener {
|
|
||||||
private @Getter T player;
|
|
||||||
private Player bukkitplayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will only send raw vanilla messages to the sender in plain text.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
*/
|
|
||||||
public VanillaCommandListener(T player) {
|
|
||||||
this.player = player;
|
|
||||||
this.bukkitplayer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
* @param bukkitplayer The Bukkit player to send the raw message to
|
|
||||||
*/
|
|
||||||
public VanillaCommandListener(T player, Player bukkitplayer) {
|
|
||||||
this.player = player;
|
|
||||||
this.bukkitplayer = bukkitplayer;
|
|
||||||
if (bukkitplayer != null && !(bukkitplayer instanceof CraftPlayer))
|
|
||||||
throw new ClassCastException("bukkitplayer must be a Bukkit player!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MinecraftServer C_() {
|
|
||||||
return ((CraftServer) Bukkit.getServer()).getServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean a(int oplevel, String cmd) {
|
|
||||||
// return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands
|
|
||||||
return oplevel == 0 || player.isOp();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return player.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public World getWorld() {
|
|
||||||
return ((CraftWorld) player.getWorld()).getHandle();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(IChatBaseComponent arg0) {
|
|
||||||
player.sendMessage(arg0.toPlainText());
|
|
||||||
if (bukkitplayer != null)
|
|
||||||
((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) {
|
|
||||||
val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase());
|
|
||||||
if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper))
|
|
||||||
return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds
|
|
||||||
|
|
||||||
if (!(dsender instanceof IMCPlayer))
|
|
||||||
throw new ClassCastException(
|
|
||||||
"dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.");
|
|
||||||
|
|
||||||
IMCPlayer<?> sender = (IMCPlayer<?>) dsender; // Don't use val on recursive interfaces :P
|
|
||||||
|
|
||||||
val vcmd = (VanillaCommandWrapper) cmd;
|
|
||||||
if (!vcmd.testPermission(sender))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
ICommandListener icommandlistener = (ICommandListener) sender.getVanillaCmdListener().getListener();
|
|
||||||
if (icommandlistener == null)
|
|
||||||
return VCMDWrapper.compatResponse(dsender);
|
|
||||||
String[] args = cmdstr.split(" ");
|
|
||||||
args = Arrays.copyOfRange(args, 1, args.length);
|
|
||||||
try {
|
|
||||||
vcmd.dispatchVanillaCommand(sender, icommandlistener, args);
|
|
||||||
} catch (CommandException commandexception) {
|
|
||||||
// Taken from CommandHandler
|
|
||||||
ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.getArgs());
|
|
||||||
chatmessage.getChatModifier().setColor(EnumChatFormat.RED);
|
|
||||||
icommandlistener.sendMessage(chatmessage);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.discordplugin.IMCPlayer;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
import net.minecraft.server.v1_14_R1.*;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.craftbukkit.v1_14_R1.CraftServer;
|
|
||||||
import org.bukkit.craftbukkit.v1_14_R1.CraftWorld;
|
|
||||||
import org.bukkit.craftbukkit.v1_14_R1.command.ProxiedNativeCommandSender;
|
|
||||||
import org.bukkit.craftbukkit.v1_14_R1.command.VanillaCommandWrapper;
|
|
||||||
import org.bukkit.craftbukkit.v1_14_R1.entity.CraftPlayer;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class VanillaCommandListener14<T extends DiscordSenderBase & IMCPlayer<T>> implements ICommandListener {
|
|
||||||
private @Getter T player;
|
|
||||||
private Player bukkitplayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will only send raw vanilla messages to the sender in plain text.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
*/
|
|
||||||
public VanillaCommandListener14(T player) {
|
|
||||||
this.player = player;
|
|
||||||
this.bukkitplayer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
* @param bukkitplayer The Bukkit player to send the raw message to
|
|
||||||
*/
|
|
||||||
public VanillaCommandListener14(T player, Player bukkitplayer) {
|
|
||||||
this.player = player;
|
|
||||||
this.bukkitplayer = bukkitplayer;
|
|
||||||
if (bukkitplayer != null && !(bukkitplayer instanceof CraftPlayer))
|
|
||||||
throw new ClassCastException("bukkitplayer must be a Bukkit player!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessage(IChatBaseComponent arg0) {
|
|
||||||
player.sendMessage(arg0.getString());
|
|
||||||
if (bukkitplayer != null)
|
|
||||||
((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldSendSuccess() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldSendFailure() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldBroadcastCommands() {
|
|
||||||
return true; //Broadcast to in-game admins
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandSender getBukkitSender(CommandListenerWrapper commandListenerWrapper) {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) {
|
|
||||||
val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase());
|
|
||||||
if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper))
|
|
||||||
return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds
|
|
||||||
|
|
||||||
if (!(dsender instanceof IMCPlayer))
|
|
||||||
throw new ClassCastException(
|
|
||||||
"dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.");
|
|
||||||
|
|
||||||
IMCPlayer<?> sender = (IMCPlayer<?>) dsender; // Don't use val on recursive interfaces :P
|
|
||||||
|
|
||||||
val vcmd = (VanillaCommandWrapper) cmd;
|
|
||||||
if (!vcmd.testPermission(sender))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
val world = ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle();
|
|
||||||
ICommandListener icommandlistener = (ICommandListener) sender.getVanillaCmdListener().getListener();
|
|
||||||
if (icommandlistener == null)
|
|
||||||
return VCMDWrapper.compatResponse(dsender);
|
|
||||||
val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0),
|
|
||||||
new Vec2F(0, 0), world, 0, sender.getName(),
|
|
||||||
new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);
|
|
||||||
val pncs = new ProxiedNativeCommandSender(wrapper, sender, sender);
|
|
||||||
String[] args = cmdstr.split(" ");
|
|
||||||
args = Arrays.copyOfRange(args, 1, args.length);
|
|
||||||
try {
|
|
||||||
return vcmd.execute(pncs, cmd.getLabel(), args);
|
|
||||||
} catch (CommandException commandexception) {
|
|
||||||
// Taken from CommandHandler
|
|
||||||
ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.a());
|
|
||||||
chatmessage.getChatModifier().setColor(EnumChatFormat.RED);
|
|
||||||
icommandlistener.sendMessage(chatmessage);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordSenderBase;
|
|
||||||
import buttondevteam.discordplugin.IMCPlayer;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.command.SimpleCommandMap;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.mockito.Answers;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link VanillaCommandListener14} but with reflection
|
|
||||||
*/
|
|
||||||
public class VanillaCommandListener15<T extends DiscordSenderBase & IMCPlayer<T>> {
|
|
||||||
private @Getter T player;
|
|
||||||
private static Class<?> vcwcl;
|
|
||||||
private static String nms;
|
|
||||||
|
|
||||||
protected VanillaCommandListener15(T player, Player bukkitplayer) {
|
|
||||||
this.player = player;
|
|
||||||
if (bukkitplayer != null && !bukkitplayer.getClass().getSimpleName().endsWith("CraftPlayer"))
|
|
||||||
throw new ClassCastException("bukkitplayer must be a Bukkit player!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will only send raw vanilla messages to the sender in plain text.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
*/
|
|
||||||
public static <T extends DiscordSenderBase & IMCPlayer<T>> VanillaCommandListener15<T> create(T player) throws Exception {
|
|
||||||
return create(player, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
|
||||||
*
|
|
||||||
* @param player The Discord sender player (the wrapper)
|
|
||||||
* @param bukkitplayer The Bukkit player to send the raw message to
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T extends DiscordSenderBase & IMCPlayer<T>> VanillaCommandListener15<T> create(T player, Player bukkitplayer) throws Exception {
|
|
||||||
if (vcwcl == null) {
|
|
||||||
String pkg = Bukkit.getServer().getClass().getPackage().getName();
|
|
||||||
vcwcl = Class.forName(pkg + ".command.VanillaCommandWrapper");
|
|
||||||
}
|
|
||||||
if (nms == null) {
|
|
||||||
var server = Bukkit.getServer();
|
|
||||||
nms = server.getClass().getMethod("getServer").invoke(server).getClass().getPackage().getName(); //org.mockito.codegen
|
|
||||||
}
|
|
||||||
var iclcl = Class.forName(nms + ".ICommandListener");
|
|
||||||
return Mockito.mock(VanillaCommandListener15.class, Mockito.withSettings().stubOnly()
|
|
||||||
.useConstructor(player, bukkitplayer).extraInterfaces(iclcl).defaultAnswer(invocation -> {
|
|
||||||
if (invocation.getMethod().getName().equals("sendMessage")) {
|
|
||||||
var icbc = invocation.getArgument(0);
|
|
||||||
player.sendMessage((String) icbc.getClass().getMethod("getString").invoke(icbc));
|
|
||||||
if (bukkitplayer != null) {
|
|
||||||
var handle = bukkitplayer.getClass().getMethod("getHandle").invoke(bukkitplayer);
|
|
||||||
handle.getClass().getMethod("sendMessage", icbc.getClass()).invoke(handle, icbc);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!Modifier.isAbstract(invocation.getMethod().getModifiers()))
|
|
||||||
return invocation.callRealMethod();
|
|
||||||
if (invocation.getMethod().getReturnType() == boolean.class)
|
|
||||||
return true; //shouldSend... shouldBroadcast...
|
|
||||||
if (invocation.getMethod().getReturnType() == CommandSender.class)
|
|
||||||
return player;
|
|
||||||
return Answers.RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) throws Exception {
|
|
||||||
var server = Bukkit.getServer();
|
|
||||||
var cmap = (SimpleCommandMap) server.getClass().getMethod("getCommandMap").invoke(server);
|
|
||||||
val cmd = cmap.getCommand(cmdstr.split(" ")[0].toLowerCase());
|
|
||||||
if (!(dsender instanceof Player) || cmd == null || !vcwcl.isAssignableFrom(cmd.getClass()))
|
|
||||||
return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds
|
|
||||||
|
|
||||||
if (!(dsender instanceof IMCPlayer))
|
|
||||||
throw new ClassCastException(
|
|
||||||
"dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.");
|
|
||||||
|
|
||||||
IMCPlayer<?> sender = (IMCPlayer<?>) dsender; // Don't use val on recursive interfaces :P
|
|
||||||
|
|
||||||
if (!(Boolean) vcwcl.getMethod("testPermission", CommandSender.class).invoke(cmd, sender))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var cworld = Bukkit.getWorlds().get(0);
|
|
||||||
val world = cworld.getClass().getMethod("getHandle").invoke(cworld);
|
|
||||||
var icommandlistener = sender.getVanillaCmdListener().getListener();
|
|
||||||
if (icommandlistener == null)
|
|
||||||
return VCMDWrapper.compatResponse(dsender);
|
|
||||||
var clwcl = Class.forName(nms + ".CommandListenerWrapper");
|
|
||||||
var v3dcl = Class.forName(nms + ".Vec3D");
|
|
||||||
var v2fcl = Class.forName(nms + ".Vec2F");
|
|
||||||
var icbcl = Class.forName(nms + ".IChatBaseComponent");
|
|
||||||
var mcscl = Class.forName(nms + ".MinecraftServer");
|
|
||||||
var ecl = Class.forName(nms + ".Entity");
|
|
||||||
var cctcl = Class.forName(nms + ".ChatComponentText");
|
|
||||||
var iclcl = Class.forName(nms + ".ICommandListener");
|
|
||||||
Object wrapper = clwcl.getConstructor(iclcl, v3dcl, v2fcl, world.getClass(), int.class, String.class, icbcl, mcscl, ecl)
|
|
||||||
.newInstance(icommandlistener,
|
|
||||||
v3dcl.getConstructor(double.class, double.class, double.class).newInstance(0, 0, 0),
|
|
||||||
v2fcl.getConstructor(float.class, float.class).newInstance(0, 0),
|
|
||||||
world, 0, sender.getName(), cctcl.getConstructor(String.class).newInstance(sender.getName()),
|
|
||||||
world.getClass().getMethod("getMinecraftServer").invoke(world), null);
|
|
||||||
/*val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0),
|
|
||||||
new Vec2F(0, 0), world, 0, sender.getName(),
|
|
||||||
new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);*/
|
|
||||||
var pncscl = Class.forName(vcwcl.getPackage().getName() + ".ProxiedNativeCommandSender");
|
|
||||||
Object pncs = pncscl.getConstructor(clwcl, CommandSender.class, CommandSender.class)
|
|
||||||
.newInstance(wrapper, sender, sender);
|
|
||||||
String[] args = cmdstr.split(" ");
|
|
||||||
args = Arrays.copyOfRange(args, 1, args.length);
|
|
||||||
try {
|
|
||||||
return cmd.execute((CommandSender) pncs, cmd.getLabel(), args);
|
|
||||||
} catch (Exception commandexception) {
|
|
||||||
if (!commandexception.getClass().getSimpleName().equals("CommandException"))
|
|
||||||
throw commandexception;
|
|
||||||
// Taken from CommandHandler
|
|
||||||
var cmcl = Class.forName(nms + ".ChatMessage");
|
|
||||||
var chatmessage = cmcl.getConstructor(String.class, Object[].class)
|
|
||||||
.newInstance(commandexception.getMessage(),
|
|
||||||
new Object[]{commandexception.getClass().getMethod("a").invoke(commandexception)});
|
|
||||||
var modifier = cmcl.getMethod("getChatModifier").invoke(chatmessage);
|
|
||||||
var ecfcl = Class.forName(nms + ".EnumChatFormat");
|
|
||||||
modifier.getClass().getMethod("setColor", ecfcl).invoke(modifier, ecfcl.getField("RED").get(null));
|
|
||||||
icommandlistener.getClass().getMethod("sendMessage", icbcl).invoke(icommandlistener, chatmessage);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
package buttondevteam.discordplugin.role;
|
|
||||||
|
|
||||||
import buttondevteam.core.ComponentManager;
|
|
||||||
import buttondevteam.discordplugin.DPUtils;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.lib.architecture.Component;
|
|
||||||
import buttondevteam.lib.architecture.ComponentMetadata;
|
|
||||||
import buttondevteam.lib.architecture.ReadOnlyConfigData;
|
|
||||||
import discord4j.core.event.domain.role.RoleCreateEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleDeleteEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleEvent;
|
|
||||||
import discord4j.core.event.domain.role.RoleUpdateEvent;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import discord4j.core.object.entity.channel.MessageChannel;
|
|
||||||
import discord4j.rest.util.Color;
|
|
||||||
import lombok.val;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically collects roles with a certain color.
|
|
||||||
* Users can add these roles to themselves using the /role Discord command.
|
|
||||||
*/
|
|
||||||
@ComponentMetadata(enabledByDefault = false)
|
|
||||||
public class GameRoleModule extends Component<DiscordPlugin> {
|
|
||||||
public List<String> GameRoles;
|
|
||||||
|
|
||||||
private final RoleCommand command = new RoleCommand(this);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enable() {
|
|
||||||
getPlugin().getManager().registerCommand(command);
|
|
||||||
GameRoles = DiscordPlugin.mainServer.getRoles().filterWhen(this::isGameRole).map(Role::getName).collect(Collectors.toList()).block();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disable() {
|
|
||||||
getPlugin().getManager().unregisterCommand(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel where the bot logs when it detects a role change that results in a new game role or one being removed.
|
|
||||||
*/
|
|
||||||
private final ReadOnlyConfigData<Mono<MessageChannel>> logChannel = DPUtils.channelData(getConfig(), "logChannel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The role color that is used by game roles.
|
|
||||||
* Defaults to the second to last in the upper row - #95a5a6.
|
|
||||||
*/
|
|
||||||
private final ReadOnlyConfigData<Color> roleColor = getConfig().<Color>getConfig("roleColor")
|
|
||||||
.def(Color.of(149, 165, 166))
|
|
||||||
.getter(rgb -> Color.of(Integer.parseInt(((String) rgb).substring(1), 16)))
|
|
||||||
.setter(color -> String.format("#%08x", color.getRGB())).buildReadOnly();
|
|
||||||
|
|
||||||
public static void handleRoleEvent(RoleEvent roleEvent) {
|
|
||||||
val grm = ComponentManager.getIfEnabled(GameRoleModule.class);
|
|
||||||
if (grm == null) return;
|
|
||||||
val GameRoles = grm.GameRoles;
|
|
||||||
val logChannel = grm.logChannel.get();
|
|
||||||
Predicate<Role> notMainServer = r -> r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong();
|
|
||||||
if (roleEvent instanceof RoleCreateEvent) {
|
|
||||||
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
|
|
||||||
Role role = ((RoleCreateEvent) roleEvent).getRole();
|
|
||||||
if (notMainServer.test(role))
|
|
||||||
return;
|
|
||||||
grm.isGameRole(role).flatMap(b -> {
|
|
||||||
if (!b)
|
|
||||||
return Mono.empty(); //Deleted or not a game role
|
|
||||||
GameRoles.add(role.getName());
|
|
||||||
if (logChannel != null)
|
|
||||||
return logChannel.flatMap(ch -> ch.createMessage("Added " + role.getName() + " as game role. If you don't want this, change the role's color from the game role color."));
|
|
||||||
return Mono.empty();
|
|
||||||
}).subscribe();
|
|
||||||
}, 100);
|
|
||||||
} else if (roleEvent instanceof RoleDeleteEvent) {
|
|
||||||
Role role = ((RoleDeleteEvent) roleEvent).getRole().orElse(null);
|
|
||||||
if (role == null) return;
|
|
||||||
if (notMainServer.test(role))
|
|
||||||
return;
|
|
||||||
if (GameRoles.remove(role.getName()) && logChannel != null)
|
|
||||||
logChannel.flatMap(ch -> ch.createMessage("Removed " + role.getName() + " as a game role.")).subscribe();
|
|
||||||
} else if (roleEvent instanceof RoleUpdateEvent) {
|
|
||||||
val event = (RoleUpdateEvent) roleEvent;
|
|
||||||
if (!event.getOld().isPresent()) {
|
|
||||||
grm.logWarn("Old role not stored, cannot update game role!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Role or = event.getOld().get();
|
|
||||||
if (notMainServer.test(or))
|
|
||||||
return;
|
|
||||||
grm.isGameRole(event.getCurrent()).flatMap(b -> {
|
|
||||||
if (!b) {
|
|
||||||
if (GameRoles.remove(or.getName()) && logChannel != null)
|
|
||||||
return logChannel.flatMap(ch -> ch.createMessage("Removed " + or.getName() + " as a game role because its color changed."));
|
|
||||||
} else {
|
|
||||||
if (GameRoles.contains(or.getName()) && or.getName().equals(event.getCurrent().getName()))
|
|
||||||
return Mono.empty();
|
|
||||||
boolean removed = GameRoles.remove(or.getName()); //Regardless of whether it was a game role
|
|
||||||
GameRoles.add(event.getCurrent().getName()); //Add it because it has no color
|
|
||||||
if (logChannel != null) {
|
|
||||||
if (removed)
|
|
||||||
return logChannel.flatMap(ch -> ch.createMessage("Changed game role from " + or.getName() + " to " + event.getCurrent().getName() + "."));
|
|
||||||
else
|
|
||||||
return logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the color of one."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Mono.empty();
|
|
||||||
}).subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Boolean> isGameRole(Role r) {
|
|
||||||
if (r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong())
|
|
||||||
return Mono.just(false); //Only allow on the main server
|
|
||||||
val rc = roleColor.get();
|
|
||||||
return Mono.just(r.getColor().equals(rc)).filter(b -> b).flatMap(b ->
|
|
||||||
DiscordPlugin.dc.getSelf().flatMap(u -> u.asMember(DiscordPlugin.mainServer.getId()))
|
|
||||||
.flatMap(m -> m.hasHigherRoles(Collections.singleton(r.getId())))) //Below one of our roles
|
|
||||||
.defaultIfEmpty(false);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package buttondevteam.discordplugin.role;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.commands.Command2DCSender;
|
|
||||||
import buttondevteam.discordplugin.commands.ICommand2DC;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import buttondevteam.lib.chat.Command2;
|
|
||||||
import buttondevteam.lib.chat.CommandClass;
|
|
||||||
import discord4j.core.object.entity.Role;
|
|
||||||
import lombok.val;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandClass
|
|
||||||
public class RoleCommand extends ICommand2DC {
|
|
||||||
|
|
||||||
private GameRoleModule grm;
|
|
||||||
|
|
||||||
RoleCommand(GameRoleModule grm) {
|
|
||||||
this.grm = grm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(helpText = {
|
|
||||||
"Add role",
|
|
||||||
"This command adds a role to your account."
|
|
||||||
})
|
|
||||||
public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) {
|
|
||||||
final Role role = checkAndGetRole(sender, rolename);
|
|
||||||
if (role == null)
|
|
||||||
return true;
|
|
||||||
try {
|
|
||||||
sender.getMessage().getAuthorAsMember()
|
|
||||||
.flatMap(m -> m.addRole(role.getId()).switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("added role."))))
|
|
||||||
.subscribe();
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Error while adding role!", e, grm);
|
|
||||||
sender.sendMessage("an error occured while adding the role.");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand(helpText = {
|
|
||||||
"Remove role",
|
|
||||||
"This command removes a role from your account."
|
|
||||||
})
|
|
||||||
public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) {
|
|
||||||
final Role role = checkAndGetRole(sender, rolename);
|
|
||||||
if (role == null)
|
|
||||||
return true;
|
|
||||||
try {
|
|
||||||
sender.getMessage().getAuthorAsMember()
|
|
||||||
.flatMap(m -> m.removeRole(role.getId()).switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("removed role."))))
|
|
||||||
.subscribe();
|
|
||||||
} catch (Exception e) {
|
|
||||||
TBMCCoreAPI.SendException("Error while removing role!", e, grm);
|
|
||||||
sender.sendMessage("an error occured while removing the role.");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command2.Subcommand
|
|
||||||
public void list(Command2DCSender sender) {
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
boolean b = false;
|
|
||||||
for (String role : (Iterable<String>) grm.GameRoles.stream().sorted()::iterator) {
|
|
||||||
sb.append(role);
|
|
||||||
if (!b)
|
|
||||||
for (int j = 0; j < Math.max(1, 20 - role.length()); j++)
|
|
||||||
sb.append(" ");
|
|
||||||
else
|
|
||||||
sb.append("\n");
|
|
||||||
b = !b;
|
|
||||||
}
|
|
||||||
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n')
|
|
||||||
sb.append('\n');
|
|
||||||
sender.sendMessage("list of roles:\n```\n" + sb + "```");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Role checkAndGetRole(Command2DCSender sender, String rolename) {
|
|
||||||
String rname = rolename;
|
|
||||||
if (!grm.GameRoles.contains(rolename)) { //If not found as-is, correct case
|
|
||||||
val orn = grm.GameRoles.stream().filter(r -> r.equalsIgnoreCase(rolename)).findAny();
|
|
||||||
if (!orn.isPresent()) {
|
|
||||||
sender.sendMessage("that role cannot be found.");
|
|
||||||
list(sender);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
rname = orn.get();
|
|
||||||
}
|
|
||||||
val frname = rname;
|
|
||||||
final List<Role> roles = DiscordPlugin.mainServer.getRoles().filter(r -> r.getName().equals(frname)).collectList().block();
|
|
||||||
if (roles == null) {
|
|
||||||
sender.sendMessage("an error occured.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (roles.size() == 0) {
|
|
||||||
sender.sendMessage("the specified role cannot be found on Discord! Removing from the list.");
|
|
||||||
grm.GameRoles.remove(rolename);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (roles.size() > 1) {
|
|
||||||
sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return roles.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package buttondevteam.discordplugin.util;
|
|
||||||
|
|
||||||
public enum DPState {
|
|
||||||
/**
|
|
||||||
* Used from server start until anything else happens
|
|
||||||
*/
|
|
||||||
RUNNING,
|
|
||||||
/**
|
|
||||||
* Used when /restart is detected
|
|
||||||
*/
|
|
||||||
RESTARTING_SERVER,
|
|
||||||
/**
|
|
||||||
* Used when the plugin is disabled by outside forces
|
|
||||||
*/
|
|
||||||
STOPPING_SERVER,
|
|
||||||
/**
|
|
||||||
* Used when /discord restart is run
|
|
||||||
*/
|
|
||||||
RESTARTING_PLUGIN,
|
|
||||||
/**
|
|
||||||
* Used when the plugin is in the RUNNING state when the chat is disabled
|
|
||||||
*/
|
|
||||||
DISABLED_MCCHAT
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package buttondevteam.discordplugin.util;
|
|
||||||
|
|
||||||
import buttondevteam.discordplugin.listeners.CommonListeners;
|
|
||||||
|
|
||||||
public class Timings {
|
|
||||||
private long start;
|
|
||||||
|
|
||||||
public Timings() {
|
|
||||||
start = System.nanoTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void printElapsed(String message) {
|
|
||||||
CommonListeners.debug(message + " (" + (System.nanoTime() - start) / 1000000L + ")");
|
|
||||||
start = System.nanoTime();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.util.DPState
|
||||||
|
import org.apache.logging.log4j.Level
|
||||||
|
import org.apache.logging.log4j.core.appender.AbstractAppender
|
||||||
|
import org.apache.logging.log4j.core.filter.LevelRangeFilter
|
||||||
|
import org.apache.logging.log4j.core.layout.PatternLayout
|
||||||
|
import org.apache.logging.log4j.core.{Filter, LogEvent}
|
||||||
|
|
||||||
|
class BukkitLogWatcher private[discordplugin]() extends AbstractAppender("ChromaDiscord",
|
||||||
|
LevelRangeFilter.createFilter(Level.INFO, Level.INFO, Filter.Result.ACCEPT, Filter.Result.DENY),
|
||||||
|
PatternLayout.createDefaultLayout) {
|
||||||
|
override def append(logEvent: LogEvent): Unit =
|
||||||
|
if (logEvent.getMessage.getFormattedMessage.contains("Attempting to restart with "))
|
||||||
|
MinecraftChatModule.state = DPState.RESTARTING_SERVER
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
object ChannelconBroadcast extends Enumeration {
|
||||||
|
type ChannelconBroadcast = Value
|
||||||
|
val JOINLEAVE, AFK, RESTART, DEATH, BROADCAST = Value
|
||||||
|
}
|
37
src/main/scala/buttondevteam/discordplugin/ChromaBot.scala
Normal file
37
src/main/scala/buttondevteam/discordplugin/ChromaBot.scala
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatUtils
|
||||||
|
import discord4j.core.`object`.entity.Message
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import javax.annotation.Nullable
|
||||||
|
|
||||||
|
object ChromaBot {
|
||||||
|
private var _enabled = false
|
||||||
|
|
||||||
|
def enabled = _enabled
|
||||||
|
|
||||||
|
private[discordplugin] def enabled_=(en: Boolean): Unit = _enabled = en
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the chat channels and private chats.
|
||||||
|
*
|
||||||
|
* @param message The message to send, duh (use [[MessageChannel.createMessage]])
|
||||||
|
*/
|
||||||
|
def sendMessage(message: SMono[MessageChannel] => SMono[Message]): Unit =
|
||||||
|
MCChatUtils.forPublicPrivateChat(message).subscribe()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the chat channels, private chats and custom chats.
|
||||||
|
*
|
||||||
|
* @param message The message to send, duh
|
||||||
|
* @param toggle The toggle type for channelcon
|
||||||
|
*/
|
||||||
|
def sendMessageCustomAsWell(message: SMono[MessageChannel] => SMono[Message], @Nullable toggle: ChannelconBroadcast): Unit =
|
||||||
|
MCChatUtils.forCustomAndAllMCChat(message.apply, toggle, hookmsg = false).subscribe()
|
||||||
|
|
||||||
|
def updatePlayerList(): Unit =
|
||||||
|
MCChatUtils.updatePlayerList()
|
||||||
|
}
|
217
src/main/scala/buttondevteam/discordplugin/DPUtils.scala
Normal file
217
src/main/scala/buttondevteam/discordplugin/DPUtils.scala
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.{Component, ConfigData, IHaveConfig, ReadOnlyConfigData}
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import discord4j.core.`object`.entity.{Guild, Message, Role}
|
||||||
|
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacySpec}
|
||||||
|
import reactor.core.publisher.{Flux, Mono}
|
||||||
|
import reactor.core.scala.publisher.{SFlux, SMono}
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
import java.util.Comparator
|
||||||
|
import java.util.logging.Logger
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import javax.annotation.Nullable
|
||||||
|
|
||||||
|
object DPUtils {
|
||||||
|
private val URL_PATTERN = Pattern.compile("https?://\\S*")
|
||||||
|
private val FORMAT_PATTERN = Pattern.compile("[*_~]")
|
||||||
|
|
||||||
|
def embedWithHead(ecs: LegacyEmbedCreateSpec, displayname: String, playername: String, profileUrl: String): LegacyEmbedCreateSpec =
|
||||||
|
ecs.setAuthor(displayname, profileUrl, "https://minotar.net/avatar/" + playername + "/32.png")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes §[char] colour codes from strings & escapes them for Discord <br>
|
||||||
|
* Ensure that this method only gets called once (escaping)
|
||||||
|
*/
|
||||||
|
def sanitizeString(string: String): String = escape(sanitizeStringNoEscape(string))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes §[char] colour codes from strings
|
||||||
|
*/
|
||||||
|
def sanitizeStringNoEscape(string: String): String = {
|
||||||
|
val sanitizedString = new StringBuilder
|
||||||
|
var random = false
|
||||||
|
var i = 0
|
||||||
|
while ( {
|
||||||
|
i < string.length
|
||||||
|
}) {
|
||||||
|
if (string.charAt(i) == '§') {
|
||||||
|
i += 1 // Skips the data value, the 4 in "§4Alisolarflare"
|
||||||
|
random = string.charAt(i) == 'k'
|
||||||
|
}
|
||||||
|
else if (!random) { // Skip random/obfuscated characters
|
||||||
|
sanitizedString.append(string.charAt(i))
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
sanitizedString.toString
|
||||||
|
}
|
||||||
|
|
||||||
|
private def escape(message: String) = { //var ts = new TreeSet<>();
|
||||||
|
val ts = new util.TreeSet[Array[Int]](Comparator.comparingInt((a: Array[Int]) => a(0)): Comparator[Array[Int]]) //Compare the start, then check the end
|
||||||
|
var matcher = URL_PATTERN.matcher(message)
|
||||||
|
while (matcher.find) ts.add(Array[Int](matcher.start, matcher.end))
|
||||||
|
matcher = FORMAT_PATTERN.matcher(message)
|
||||||
|
val sb = new StringBuffer
|
||||||
|
while (matcher.find) matcher.appendReplacement(sb, if (Option(ts.floor(Array[Int](matcher.start, 0))).map( //Find a URL start <= our start
|
||||||
|
(a: Array[Int]) => a(1)).getOrElse(-1) < matcher.start //Check if URL end < our start
|
||||||
|
) "\\\\" + matcher.group else matcher.group)
|
||||||
|
matcher.appendTail(sb)
|
||||||
|
sb.toString
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogger: Logger = {
|
||||||
|
if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger == null) Logger.getLogger("DiscordPlugin")
|
||||||
|
else DiscordPlugin.plugin.getLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
def channelData(config: IHaveConfig, key: String): ReadOnlyConfigData[SMono[MessageChannel]] =
|
||||||
|
config.getReadOnlyDataPrimDef(key, 0L, (id: Any) =>
|
||||||
|
getMessageChannel(key, Snowflake.of(id.asInstanceOf[Long])), (_: SMono[MessageChannel]) => 0L) //We can afford to search for the channel in the cache once (instead of using mainServer)
|
||||||
|
|
||||||
|
def roleData(config: IHaveConfig, key: String, defName: String): ReadOnlyConfigData[SMono[Role]] =
|
||||||
|
roleData(config, key, defName, SMono.just(DiscordPlugin.mainServer))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Needs to be a [[ConfigData]] for checking if it's set
|
||||||
|
*/
|
||||||
|
def roleData(config: IHaveConfig, key: String, defName: String, guild: SMono[Guild]): ReadOnlyConfigData[SMono[Role]] = config.getReadOnlyDataPrimDef(key, defName, (name: Any) => {
|
||||||
|
def foo(name: Any): SMono[Role] = {
|
||||||
|
if (!name.isInstanceOf[String] || name.asInstanceOf[String].isEmpty) return SMono.empty[Role]
|
||||||
|
guild.flatMapMany(_.getRoles).filter((r: Role) => r.getName == name).onErrorResume((e: Throwable) => {
|
||||||
|
def foo(e: Throwable): SMono[Role] = {
|
||||||
|
getLogger.warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage)
|
||||||
|
SMono.empty[Role]
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(e)
|
||||||
|
}).next
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(name)
|
||||||
|
}, (_: SMono[Role]) => defName)
|
||||||
|
|
||||||
|
def snowflakeData(config: IHaveConfig, key: String, defID: Long): ReadOnlyConfigData[Snowflake] =
|
||||||
|
config.getReadOnlyDataPrimDef(key, defID, (id: Any) => Snowflake.of(id.asInstanceOf[Long]), _.asLong)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mentions the <b>bot channel</b>. Useful for help texts.
|
||||||
|
*
|
||||||
|
* @return The string for mentioning the channel
|
||||||
|
*/
|
||||||
|
def botmention: String = {
|
||||||
|
if (DiscordPlugin.plugin == null) return "#bot"
|
||||||
|
channelMention(DiscordPlugin.plugin.commandChannel.get)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the component if one of the given configs return null. Useful for channel/role configs.
|
||||||
|
*
|
||||||
|
* @param component The component to disable if needed
|
||||||
|
* @param configs The configs to check for null
|
||||||
|
* @return Whether the component got disabled and a warning logged
|
||||||
|
*/
|
||||||
|
def disableIfConfigError(@Nullable component: Component[DiscordPlugin], configs: ConfigData[_]*): Boolean = {
|
||||||
|
for (config <- configs) {
|
||||||
|
val v = config.get
|
||||||
|
if (disableIfConfigErrorRes(component, config, v)) return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the component if one of the given configs return null. Useful for channel/role configs.
|
||||||
|
*
|
||||||
|
* @param component The component to disable if needed
|
||||||
|
* @param config The (snowflake) config to check for null
|
||||||
|
* @param result The result of getting the value
|
||||||
|
* @return Whether the component got disabled and a warning logged
|
||||||
|
*/
|
||||||
|
def disableIfConfigErrorRes(@Nullable component: Component[DiscordPlugin], config: ConfigData[_], result: Any): Boolean = {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (result == null || (result.isInstanceOf[SMono[_]] && !result.asInstanceOf[SMono[_]].hasElement.block())) {
|
||||||
|
var path: String = null
|
||||||
|
try {
|
||||||
|
if (component != null) Component.setComponentEnabled(component, false)
|
||||||
|
path = config.getPath
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
if (component != null) TBMCCoreAPI.SendException("Failed to disable component after config error!", e, component)
|
||||||
|
else TBMCCoreAPI.SendException("Failed to disable component after config error!", e, DiscordPlugin.plugin)
|
||||||
|
}
|
||||||
|
getLogger.warning("The config value " + path + " isn't set correctly " + (if (component == null) "in global settings!"
|
||||||
|
else "for component " + component.getClass.getSimpleName + "!"))
|
||||||
|
getLogger.warning("Set the correct ID in the config" + (if (component == null) ""
|
||||||
|
else " or disable this component") + " to remove this message.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response in the form of "@User, message". Use SMono.empty() if you don't have a channel object.
|
||||||
|
*
|
||||||
|
* @param original The original message to reply to
|
||||||
|
* @param channel The channel to send the message in, defaults to the original
|
||||||
|
* @param message The message to send
|
||||||
|
* @return A mono to send the message
|
||||||
|
*/
|
||||||
|
def reply(original: Message, @Nullable channel: MessageChannel, message: String): SMono[Message] = {
|
||||||
|
val ch = if (channel == null) SMono(original.getChannel)
|
||||||
|
else SMono.just(channel)
|
||||||
|
reply(original, ch, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #reply(Message, MessageChannel, String)
|
||||||
|
*/
|
||||||
|
def reply(original: Message, ch: SMono[MessageChannel], message: String): SMono[Message] =
|
||||||
|
ch.flatMap(channel => SMono(channel.createMessage((if (original.getAuthor.isPresent)
|
||||||
|
original.getAuthor.get.getMention + ", "
|
||||||
|
else "") + message)))
|
||||||
|
|
||||||
|
def nickMention(userId: Snowflake): String = "<@!" + userId.asString + ">"
|
||||||
|
|
||||||
|
def channelMention(channelId: Snowflake): String = "<#" + channelId.asString + ">"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a message channel for a config. Returns empty for ID 0.
|
||||||
|
*
|
||||||
|
* @param key The config key
|
||||||
|
* @param id The channel ID
|
||||||
|
* @return A message channel
|
||||||
|
*/
|
||||||
|
def getMessageChannel(key: String, id: Snowflake): SMono[MessageChannel] = {
|
||||||
|
if (id.asLong == 0L) return SMono.empty[MessageChannel]
|
||||||
|
|
||||||
|
SMono(DiscordPlugin.dc.getChannelById(id)).onErrorResume(e => {
|
||||||
|
def foo(e: Throwable) = {
|
||||||
|
getLogger.warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage)
|
||||||
|
SMono.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(e)
|
||||||
|
}).filter(ch => ch.isInstanceOf[MessageChannel]).cast[MessageChannel]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMessageChannel(config: ConfigData[Snowflake]): SMono[MessageChannel] =
|
||||||
|
getMessageChannel(config.getPath, config.get)
|
||||||
|
|
||||||
|
def ignoreError[T](mono: SMono[T]): SMono[T] = mono.onErrorResume((_: Throwable) => SMono.empty)
|
||||||
|
|
||||||
|
implicit class MonoExtensions[T](mono: Mono[T]) {
|
||||||
|
def ^^(): SMono[T] = SMono(mono)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit class FluxExtensions[T](flux: Flux[T]) {
|
||||||
|
def ^^(): SFlux[T] = SFlux(flux)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit class SpecExtensions[T <: LegacySpec[_]](spec: T) {
|
||||||
|
def ^^(): Unit = ()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.playerfaker.{DiscordInventory, VCMDWrapper}
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import org.bukkit.*
|
||||||
|
import org.bukkit.attribute.{Attribute, AttributeInstance, AttributeModifier}
|
||||||
|
import org.bukkit.entity.{Entity, Player}
|
||||||
|
import org.bukkit.event.player.{AsyncPlayerChatEvent, PlayerTeleportEvent}
|
||||||
|
import org.bukkit.inventory.{Inventory, PlayerInventory}
|
||||||
|
import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo}
|
||||||
|
import org.bukkit.plugin.Plugin
|
||||||
|
import org.mockito.Answers.RETURNS_DEFAULTS
|
||||||
|
import org.mockito.invocation.InvocationOnMock
|
||||||
|
import org.mockito.{MockSettings, Mockito}
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.util
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object DiscordConnectedPlayer {
|
||||||
|
def create(user: User, channel: MessageChannel, uuid: UUID, mcname: String, module: MinecraftChatModule): DiscordConnectedPlayer =
|
||||||
|
Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(user, channel, uuid, mcname, module))
|
||||||
|
|
||||||
|
def createTest: DiscordConnectedPlayer =
|
||||||
|
Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(null, null))
|
||||||
|
|
||||||
|
private def getSettings: MockSettings = Mockito.withSettings.defaultAnswer((invocation: InvocationOnMock) => {
|
||||||
|
def foo(invocation: InvocationOnMock): AnyRef =
|
||||||
|
try {
|
||||||
|
if (!Modifier.isAbstract(invocation.getMethod.getModifiers))
|
||||||
|
invocation.callRealMethod
|
||||||
|
else if (classOf[PlayerInventory].isAssignableFrom(invocation.getMethod.getReturnType))
|
||||||
|
Mockito.mock(classOf[DiscordInventory], Mockito.withSettings.extraInterfaces(classOf[PlayerInventory]))
|
||||||
|
else if (classOf[Inventory].isAssignableFrom(invocation.getMethod.getReturnType))
|
||||||
|
new DiscordInventory
|
||||||
|
else
|
||||||
|
RETURNS_DEFAULTS.answer(invocation)
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
System.err.println("Error in mocked player!")
|
||||||
|
e.printStackTrace()
|
||||||
|
RETURNS_DEFAULTS.answer(invocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(invocation)
|
||||||
|
}).stubOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor The parameters must match with [[DiscordConnectedPlayer.create]]
|
||||||
|
* @param user May be null.
|
||||||
|
* @param channel May not be null.
|
||||||
|
* @param uniqueId The UUID of the player.
|
||||||
|
* @param name The Minecraft name of the player.
|
||||||
|
* @param module The MinecraftChatModule or null if testing.
|
||||||
|
*/
|
||||||
|
abstract class DiscordConnectedPlayer(user: User, channel: MessageChannel, val uniqueId: UUID, val name: String, val module: MinecraftChatModule) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordConnectedPlayer] {
|
||||||
|
private var loggedIn = false
|
||||||
|
private var displayName: String = name
|
||||||
|
|
||||||
|
private var location: Location = if (module == null) null else Bukkit.getWorlds.get(0).getSpawnLocation
|
||||||
|
private val basePlayer: OfflinePlayer = if (module == null) null else Bukkit.getOfflinePlayer(uniqueId)
|
||||||
|
private var perm: PermissibleBase = if (module == null) null else new PermissibleBase(basePlayer)
|
||||||
|
private val origPerm: PermissibleBase = perm
|
||||||
|
private val vanillaCmdListener: VCMDWrapper = if (module == null) null else new VCMDWrapper(VCMDWrapper.createListener(this, module))
|
||||||
|
|
||||||
|
override def isPermissionSet(name: String): Boolean = this.origPerm.isPermissionSet(name)
|
||||||
|
|
||||||
|
override def isPermissionSet(perm: Permission): Boolean = this.origPerm.isPermissionSet(perm)
|
||||||
|
|
||||||
|
override def hasPermission(inName: String): Boolean = this.origPerm.hasPermission(inName)
|
||||||
|
|
||||||
|
override def hasPermission(perm: Permission): Boolean = this.origPerm.hasPermission(perm)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = this.origPerm.addAttachment(plugin, name, value)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin): PermissionAttachment = this.origPerm.addAttachment(plugin)
|
||||||
|
|
||||||
|
override def removeAttachment(attachment: PermissionAttachment): Unit = this.origPerm.removeAttachment(attachment)
|
||||||
|
|
||||||
|
override def recalculatePermissions(): Unit = this.origPerm.recalculatePermissions()
|
||||||
|
|
||||||
|
def clearPermissions(): Unit = this.origPerm.clearPermissions()
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment =
|
||||||
|
this.origPerm.addAttachment(plugin, name, value, ticks)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = this.origPerm.addAttachment(plugin, ticks)
|
||||||
|
|
||||||
|
override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = this.origPerm.getEffectivePermissions
|
||||||
|
|
||||||
|
def setLoggedIn(loggedIn: Boolean): Unit = this.loggedIn = loggedIn
|
||||||
|
|
||||||
|
def setPerm(perm: PermissibleBase): Unit = this.perm = perm
|
||||||
|
|
||||||
|
override def setDisplayName(displayName: String): Unit = this.displayName = displayName
|
||||||
|
|
||||||
|
override def getVanillaCmdListener: VCMDWrapper = this.vanillaCmdListener
|
||||||
|
|
||||||
|
def isLoggedIn: Boolean = this.loggedIn
|
||||||
|
|
||||||
|
override def getName: String = this.name
|
||||||
|
|
||||||
|
def getBasePlayer: OfflinePlayer = this.basePlayer
|
||||||
|
|
||||||
|
def getPerm: PermissibleBase = this.perm
|
||||||
|
|
||||||
|
override def getUniqueId: UUID = this.uniqueId
|
||||||
|
|
||||||
|
override def getDisplayName: String = this.displayName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing
|
||||||
|
*/
|
||||||
|
def this(user: User, channel: MessageChannel) =
|
||||||
|
this(user, channel, UUID.randomUUID(), "Test", null)
|
||||||
|
|
||||||
|
override def setOp(value: Boolean): Unit = { //CraftPlayer-compatible implementation
|
||||||
|
this.origPerm.setOp(value)
|
||||||
|
this.perm.recalculatePermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isOp: Boolean = this.origPerm.isOp
|
||||||
|
|
||||||
|
override def teleport(location: Location): Boolean = {
|
||||||
|
if (module.allowFakePlayerTeleports.get) this.location = location
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
def teleport(location: Location, cause: PlayerTeleportEvent.TeleportCause): Boolean = {
|
||||||
|
if (module.allowFakePlayerTeleports.get) this.location = location
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override def teleport(destination: Entity): Boolean = {
|
||||||
|
if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
def teleport(destination: Entity, cause: PlayerTeleportEvent.TeleportCause): Boolean = {
|
||||||
|
if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getLocation(loc: Location): Location = {
|
||||||
|
if (loc != null) {
|
||||||
|
loc.setWorld(getWorld)
|
||||||
|
loc.setX(location.getX)
|
||||||
|
loc.setY(location.getY)
|
||||||
|
loc.setZ(location.getZ)
|
||||||
|
loc.setYaw(location.getYaw)
|
||||||
|
loc.setPitch(location.getPitch)
|
||||||
|
}
|
||||||
|
loc
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getServer: Server = Bukkit.getServer
|
||||||
|
|
||||||
|
override def sendRawMessage(message: String): Unit = sendMessage(message)
|
||||||
|
|
||||||
|
override def chat(msg: String): Unit = Bukkit.getPluginManager.callEvent(new AsyncPlayerChatEvent(true, this, msg, new util.HashSet[Player](Bukkit.getOnlinePlayers)))
|
||||||
|
|
||||||
|
override def getWorld: World = Bukkit.getWorlds.get(0)
|
||||||
|
|
||||||
|
override def isOnline = true
|
||||||
|
|
||||||
|
override def getLocation = new Location(getWorld, location.getX, location.getY, location.getZ, location.getYaw, location.getPitch)
|
||||||
|
|
||||||
|
override def getEyeLocation: Location = getLocation
|
||||||
|
|
||||||
|
@deprecated override def getMaxHealth = 20d
|
||||||
|
|
||||||
|
override def getPlayer: DiscordConnectedPlayer = this
|
||||||
|
|
||||||
|
override def getAttribute(attribute: Attribute): AttributeInstance = new AttributeInstance() {
|
||||||
|
override def getAttribute: Attribute = attribute
|
||||||
|
|
||||||
|
override def getBaseValue: Double = getDefaultValue
|
||||||
|
|
||||||
|
override def setBaseValue(value: Double): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getModifiers: util.Collection[AttributeModifier] = Collections.emptyList
|
||||||
|
|
||||||
|
override def addModifier(modifier: AttributeModifier): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def removeModifier(modifier: AttributeModifier): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getValue: Double = getDefaultValue
|
||||||
|
|
||||||
|
override def getDefaultValue: Double = 20 //Works for max health, should be okay for the rest
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getGameMode = GameMode.SPECTATOR
|
||||||
|
|
||||||
|
//noinspection ScalaDeprecation
|
||||||
|
/*@SuppressWarnings(Array("deprecation")) override def spigot: super.Spigot = new super.Spigot() {
|
||||||
|
override def getRawAddress: InetSocketAddress = null
|
||||||
|
|
||||||
|
override def playEffect(location: Location, effect: Effect, id: Int, data: Int, offsetX: Float, offsetY: Float, offsetZ: Float, speed: Float, particleCount: Int, radius: Int): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getCollidesWithEntities = false
|
||||||
|
|
||||||
|
override def setCollidesWithEntities(collides: Boolean): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def respawn(): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getLocale = "en_us"
|
||||||
|
|
||||||
|
override def getHiddenPlayers: util.Set[Player] = Collections.emptySet
|
||||||
|
|
||||||
|
override def sendMessage(component: BaseComponent): Unit =
|
||||||
|
DiscordConnectedPlayer.super.sendMessage(component.toPlainText)
|
||||||
|
|
||||||
|
override def sendMessage(components: BaseComponent*): Unit =
|
||||||
|
for (component <- components)
|
||||||
|
sendMessage(component)
|
||||||
|
|
||||||
|
override def sendMessage(position: ChatMessageType, component: BaseComponent): Unit =
|
||||||
|
sendMessage(component) //Ignore position
|
||||||
|
override def sendMessage(position: ChatMessageType, components: BaseComponent*): Unit =
|
||||||
|
sendMessage(components: _*)
|
||||||
|
|
||||||
|
override def isInvulnerable = true
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatPrivate
|
||||||
|
import buttondevteam.lib.player.{ChromaGamerBase, UserClass}
|
||||||
|
|
||||||
|
@UserClass(foldername = "discord") class DiscordPlayer() extends ChromaGamerBase {
|
||||||
|
private var did: String = null
|
||||||
|
|
||||||
|
// private @Getter @Setter boolean minecraftChatEnabled;
|
||||||
|
def getDiscordID: String = {
|
||||||
|
if (did == null) did = getFileName
|
||||||
|
did
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if player has the private Minecraft chat enabled. For setting the value, see
|
||||||
|
* [[MCChatPrivate.privateMCChat]]
|
||||||
|
*/
|
||||||
|
def isMinecraftChatEnabled: Boolean = MCChatPrivate.isMinecraftChatEnabled(this)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VCMDWrapper
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.invocation.InvocationOnMock
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
object DiscordPlayerSender {
|
||||||
|
def create(user: User, channel: MessageChannel, player: Player, module: MinecraftChatModule): DiscordPlayerSender =
|
||||||
|
Mockito.mock(classOf[DiscordPlayerSender], Mockito.withSettings.stubOnly.defaultAnswer((invocation: InvocationOnMock) => {
|
||||||
|
def foo(invocation: InvocationOnMock): AnyRef = {
|
||||||
|
if (!Modifier.isAbstract(invocation.getMethod.getModifiers))
|
||||||
|
invocation.callRealMethod
|
||||||
|
else
|
||||||
|
invocation.getMethod.invoke(invocation.getMock.asInstanceOf[DiscordPlayerSender].player, invocation.getArguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(invocation)
|
||||||
|
}).useConstructor(user, channel, player, module))
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DiscordPlayerSender(user: User, channel: MessageChannel, var player: Player, val module: Nothing) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordPlayerSender] {
|
||||||
|
val vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player, module))
|
||||||
|
|
||||||
|
override def getVanillaCmdListener: VCMDWrapper = this.vanillaCmdListener
|
||||||
|
|
||||||
|
override def sendMessage(message: String): Unit = {
|
||||||
|
player.sendMessage(message)
|
||||||
|
super.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def sendMessage(messages: Array[String]): Unit = {
|
||||||
|
player.sendMessage(messages)
|
||||||
|
super.sendMessage(messages)
|
||||||
|
}
|
||||||
|
}
|
264
src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala
Normal file
264
src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin.dc
|
||||||
|
import buttondevteam.discordplugin.announcer.AnnouncerModule
|
||||||
|
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule
|
||||||
|
import buttondevteam.discordplugin.commands.*
|
||||||
|
import buttondevteam.discordplugin.exceptions.ExceptionListenerModule
|
||||||
|
import buttondevteam.discordplugin.fun.FunModule
|
||||||
|
import buttondevteam.discordplugin.listeners.{CommonListeners, MCListener}
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.mccommands.DiscordMCCommand
|
||||||
|
import buttondevteam.discordplugin.role.GameRoleModule
|
||||||
|
import buttondevteam.discordplugin.util.{DPState, Timings}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.*
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase
|
||||||
|
import com.google.common.io.Files
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.{ApplicationInfo, Guild, Role}
|
||||||
|
import discord4j.core.`object`.presence.{Activity, ClientActivity, ClientPresence, Presence}
|
||||||
|
import discord4j.core.`object`.reaction.ReactionEmoji
|
||||||
|
import discord4j.core.event.domain.guild.GuildCreateEvent
|
||||||
|
import discord4j.core.event.domain.lifecycle.ReadyEvent
|
||||||
|
import discord4j.core.{DiscordClientBuilder, GatewayDiscordClient}
|
||||||
|
import discord4j.gateway.ShardInfo
|
||||||
|
import discord4j.rest.interaction.Interactions
|
||||||
|
import discord4j.store.jdk.JdkStoreService
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import org.apache.logging.log4j.core.Logger
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration
|
||||||
|
import org.mockito.internal.util.MockUtil
|
||||||
|
import reactor.core.Disposable
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
@ButtonPlugin.ConfigOpts(disableConfigGen = true) object DiscordPlugin {
|
||||||
|
private[discordplugin] var dc: GatewayDiscordClient = null
|
||||||
|
private[discordplugin] var plugin: DiscordPlugin = null
|
||||||
|
private[discordplugin] var SafeMode = true
|
||||||
|
|
||||||
|
def getPrefix: Char = {
|
||||||
|
if (plugin == null) '/'
|
||||||
|
else plugin.prefix.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private[discordplugin] var mainServer: Guild = null
|
||||||
|
private[discordplugin] val DELIVERED_REACTION = ReactionEmoji.unicode("✅")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ButtonPlugin.ConfigOpts(disableConfigGen = true) class DiscordPlugin extends ButtonPlugin {
|
||||||
|
private var _manager: Command2DC = null
|
||||||
|
|
||||||
|
def manager: Command2DC = _manager
|
||||||
|
|
||||||
|
private var starting = false
|
||||||
|
private var logWatcher: BukkitLogWatcher = null
|
||||||
|
/**
|
||||||
|
* The prefix to use with Discord commands like /role. It only works in the bot channel.
|
||||||
|
*/
|
||||||
|
final private val prefix = getIConfig.getData("prefix", '/', (str: Any) => str.asInstanceOf[String].charAt(0), (_: Char).toString)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
|
||||||
|
*/
|
||||||
|
private def mainServer = getIConfig.getDataPrimDef("mainServer", 0L, (id: Any) => {
|
||||||
|
def foo(id: Any): Option[Guild] = { //It attempts to get the default as well
|
||||||
|
if (id.asInstanceOf[Long] == 0L) Option.empty
|
||||||
|
else SMono.fromPublisher(DiscordPlugin.dc.getGuildById(Snowflake.of(id.asInstanceOf[Long])))
|
||||||
|
.onErrorResume((t: Throwable) => {
|
||||||
|
getLogger.warning("Failed to get guild: " + t.getMessage);
|
||||||
|
SMono.empty
|
||||||
|
}).blockOption()
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(id)
|
||||||
|
}, (g: Option[Guild]) => (g.map(_.getId.asLong): Option[Long]).getOrElse(0L))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The (bot) channel to use for Discord commands like /role.
|
||||||
|
*/
|
||||||
|
var commandChannel: ReadOnlyConfigData[Snowflake] = DPUtils.snowflakeData(getIConfig, "commandChannel", 0L)
|
||||||
|
/**
|
||||||
|
* The role that allows using mod-only Discord commands.
|
||||||
|
* If empty (''), then it will only allow for the owner.
|
||||||
|
*/
|
||||||
|
var modRole: ReadOnlyConfigData[SMono[Role]] = null
|
||||||
|
/**
|
||||||
|
* The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
|
||||||
|
*/
|
||||||
|
var inviteLink: ConfigData[String] = getIConfig.getData("inviteLink", "")
|
||||||
|
|
||||||
|
private def setupConfig(): Unit = modRole = DPUtils.roleData(getIConfig, "modRole", "Moderator")
|
||||||
|
|
||||||
|
override def onLoad(): Unit = { //Needed by ServerWatcher
|
||||||
|
val thread = Thread.currentThread
|
||||||
|
val cl = thread.getContextClassLoader
|
||||||
|
thread.setContextClassLoader(getClassLoader)
|
||||||
|
MockUtil.isMock(null) //Load MockUtil to load Mockito plugins
|
||||||
|
thread.setContextClassLoader(cl)
|
||||||
|
getLogger.info("Load complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def pluginEnable(): Unit = try {
|
||||||
|
getLogger.info("Initializing...")
|
||||||
|
DiscordPlugin.plugin = this
|
||||||
|
_manager = new Command2DC
|
||||||
|
registerCommand(new DiscordMCCommand) //Register so that the restart command works
|
||||||
|
var token: String = null
|
||||||
|
val tokenFile = new File("TBMC", "Token.txt")
|
||||||
|
if (tokenFile.exists) { //Legacy support
|
||||||
|
//noinspection UnstableApiUsage
|
||||||
|
token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val privateFile = new File(getDataFolder, "private.yml")
|
||||||
|
val conf = YamlConfiguration.loadConfiguration(privateFile)
|
||||||
|
token = conf.getString("token")
|
||||||
|
if (token == null || token.equalsIgnoreCase("Token goes here")) {
|
||||||
|
conf.set("token", "Token goes here")
|
||||||
|
conf.save(privateFile)
|
||||||
|
getLogger.severe("Token not found! Please set it in private.yml then do /discord restart")
|
||||||
|
getLogger.severe("You need to have a bot account to use with your server.")
|
||||||
|
getLogger.severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.")
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
starting = true
|
||||||
|
//System.out.println("This line should show up for sure");
|
||||||
|
val cb = DiscordClientBuilder.create(token).build.gateway
|
||||||
|
//System.out.println("Got gateway bootstrap");
|
||||||
|
cb.setInitialPresence((si: ShardInfo) => ClientPresence.doNotDisturb(ClientActivity.playing("booting")))
|
||||||
|
//cb.setStore(new JdkStoreService) //The default doesn't work for some reason - it's waaay faster now
|
||||||
|
//System.out.println("Initial status and store service set");
|
||||||
|
cb.login.doOnError((t: Throwable) => {
|
||||||
|
def foo(t: Throwable): Unit = {
|
||||||
|
stopStarting()
|
||||||
|
//System.out.println("Got this error: " + t); t.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(t)
|
||||||
|
}).subscribe((dc: GatewayDiscordClient) => {
|
||||||
|
DiscordPlugin.dc = dc //Set to gateway client
|
||||||
|
dc.on(classOf[ReadyEvent]).map(_.getGuilds.size).flatMap(dc.on(classOf[GuildCreateEvent]).take(_).collectList)
|
||||||
|
.doOnError(_ => stopStarting()).subscribe(this.handleReady _) // Take all received GuildCreateEvents and make it a List
|
||||||
|
()
|
||||||
|
}) /* All guilds have been received, client is fully connected */
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this)
|
||||||
|
getLogger.severe("You may be able to restart the plugin using /discord restart")
|
||||||
|
stopStarting()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def stopStarting(): Unit = {
|
||||||
|
this synchronized {
|
||||||
|
starting = false
|
||||||
|
notifyAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def handleReady(event: java.util.List[GuildCreateEvent]): Unit = { //System.out.println("Got ready event");
|
||||||
|
try {
|
||||||
|
if (DiscordPlugin.mainServer != null) { //This is not the first ready event
|
||||||
|
getLogger.info("Ready event already handled") //TODO: It should probably handle disconnections
|
||||||
|
DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe //Update from the initial presence
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
DiscordPlugin.mainServer = mainServer.get.orNull //Shouldn't change afterwards
|
||||||
|
if (DiscordPlugin.mainServer == null) {
|
||||||
|
if (event.size == 0) {
|
||||||
|
getLogger.severe("Main server not found! Invite the bot and do /discord restart")
|
||||||
|
DiscordPlugin.dc.getApplicationInfo.subscribe((info: ApplicationInfo) => getLogger.severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId.asString + "&scope=bot&permissions=268509264"))
|
||||||
|
saveConfig() //Put default there
|
||||||
|
return () //We should have all guilds by now, no need to retry
|
||||||
|
}
|
||||||
|
DiscordPlugin.mainServer = event.get(0).getGuild
|
||||||
|
getLogger.warning("Main server set to first one: " + DiscordPlugin.mainServer.getName)
|
||||||
|
mainServer.set(Option(DiscordPlugin.mainServer)) //Save in config
|
||||||
|
}
|
||||||
|
DiscordPlugin.SafeMode = false
|
||||||
|
setupConfig()
|
||||||
|
DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel))
|
||||||
|
//Won't disable, just prints the warning here
|
||||||
|
if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) {
|
||||||
|
stopStarting()
|
||||||
|
return () //Reusing that field to check if stopping while still initializing
|
||||||
|
}
|
||||||
|
CommonListeners.register(DiscordPlugin.dc.getEventDispatcher)
|
||||||
|
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener, this)
|
||||||
|
TBMCCoreAPI.RegisterUserClass(classOf[DiscordPlayer], () => new DiscordPlayer)
|
||||||
|
ChromaGamerBase.addConverter((sender: CommandSender) => Optional.ofNullable(sender match {
|
||||||
|
case dsender: DiscordSenderBase => dsender.getChromaUser
|
||||||
|
case _ => null
|
||||||
|
}))
|
||||||
|
IHaveConfig.pregenConfig(this, null)
|
||||||
|
ChromaBot.enabled = true //Initialize ChromaBot
|
||||||
|
Component.registerComponent(this, new GeneralEventBroadcasterModule)
|
||||||
|
Component.registerComponent(this, new MinecraftChatModule)
|
||||||
|
Component.registerComponent(this, new ExceptionListenerModule)
|
||||||
|
Component.registerComponent(this, new GameRoleModule) //Needs the mainServer to be set
|
||||||
|
Component.registerComponent(this, new AnnouncerModule)
|
||||||
|
Component.registerComponent(this, new FunModule)
|
||||||
|
ChromaBot.updatePlayerList() //The MCChatModule is tested to be enabled
|
||||||
|
val applicationId = dc.getRestClient.getApplicationId.block()
|
||||||
|
val guildId = Some(DiscordPlugin.mainServer.getId.asLong())
|
||||||
|
manager.registerCommand(new VersionCommand, applicationId, guildId)
|
||||||
|
manager.registerCommand(new UserinfoCommand, applicationId, guildId)
|
||||||
|
manager.registerCommand(new HelpCommand, applicationId, guildId)
|
||||||
|
manager.registerCommand(new DebugCommand, applicationId, guildId)
|
||||||
|
manager.registerCommand(new ConnectCommand, applicationId, guildId)
|
||||||
|
TBMCCoreAPI.SendUnsentExceptions()
|
||||||
|
TBMCCoreAPI.SendUnsentDebugMessages()
|
||||||
|
val blw = new BukkitLogWatcher
|
||||||
|
blw.start()
|
||||||
|
LogManager.getRootLogger.asInstanceOf[Logger].addAppender(blw)
|
||||||
|
logWatcher = blw
|
||||||
|
if (!TBMCCoreAPI.IsTestServer) DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe()
|
||||||
|
else DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("testing"))).subscribe()
|
||||||
|
getLogger.info("Loaded!")
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this)
|
||||||
|
}
|
||||||
|
stopStarting()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def pluginPreDisable(): Unit = {
|
||||||
|
if (MinecraftChatModule.state eq DPState.RUNNING) MinecraftChatModule.state = DPState.STOPPING_SERVER
|
||||||
|
this synchronized {
|
||||||
|
if (starting) try wait(10000)
|
||||||
|
catch {
|
||||||
|
case e: InterruptedException =>
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ChromaBot.enabled) return () //Failed to load
|
||||||
|
val timings = new Timings
|
||||||
|
timings.printElapsed("Disable start")
|
||||||
|
timings.printElapsed("Updating player list")
|
||||||
|
ChromaBot.updatePlayerList()
|
||||||
|
timings.printElapsed("Done")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def pluginDisable(): Unit = {
|
||||||
|
val timings = new Timings
|
||||||
|
timings.printElapsed("Actual disable start (logout)")
|
||||||
|
if (!ChromaBot.enabled) return ()
|
||||||
|
try {
|
||||||
|
DiscordPlugin.SafeMode = true // Stop interacting with Discord
|
||||||
|
ChromaBot.enabled = false
|
||||||
|
LogManager.getRootLogger.asInstanceOf[Logger].removeAppender(logWatcher)
|
||||||
|
timings.printElapsed("Logging out...")
|
||||||
|
DiscordPlugin.dc.logout.block
|
||||||
|
DiscordPlugin.mainServer = null //Allow ReadyEvent again
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo}
|
||||||
|
import org.bukkit.plugin.Plugin
|
||||||
|
import org.bukkit.{Bukkit, Server}
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
|
||||||
|
class DiscordSender(user: User, channel: MessageChannel, pname: String) extends DiscordSenderBase(user, channel) with CommandSender {
|
||||||
|
private val perm = new PermissibleBase(this)
|
||||||
|
private val name: String = Option(pname)
|
||||||
|
.orElse(Option(user).flatMap(u => SMono(u.asMember(DiscordPlugin.mainServer.getId))
|
||||||
|
.onErrorResume(_ => SMono.empty).blockOption()
|
||||||
|
.map(u => u.getDisplayName)))
|
||||||
|
.getOrElse("Discord user")
|
||||||
|
|
||||||
|
def this(user: User, channel: MessageChannel) = {
|
||||||
|
this(user, channel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isPermissionSet(name: String): Boolean = perm.isPermissionSet(name)
|
||||||
|
|
||||||
|
override def isPermissionSet(perm: Permission): Boolean = this.perm.isPermissionSet(perm)
|
||||||
|
|
||||||
|
override def hasPermission(name: String): Boolean = {
|
||||||
|
if (name.contains("essentials") && !(name == "essentials.list")) false
|
||||||
|
else perm.hasPermission(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def hasPermission(perm: Permission): Boolean = this.perm.hasPermission(perm)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = perm.addAttachment(plugin, name, value)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin): PermissionAttachment = perm.addAttachment(plugin)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, name, value, ticks)
|
||||||
|
|
||||||
|
override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, ticks)
|
||||||
|
|
||||||
|
override def removeAttachment(attachment: PermissionAttachment): Unit = perm.removeAttachment(attachment)
|
||||||
|
|
||||||
|
override def recalculatePermissions(): Unit = perm.recalculatePermissions()
|
||||||
|
|
||||||
|
override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = perm.getEffectivePermissions
|
||||||
|
|
||||||
|
override def isOp = false
|
||||||
|
|
||||||
|
override def setOp(value: Boolean): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getServer: Server = Bukkit.getServer
|
||||||
|
|
||||||
|
override def getName: String = name
|
||||||
|
|
||||||
|
//override def spigot(): CommandSender.Spigot = new CommandSender.Spigot
|
||||||
|
override def spigot(): CommandSender.Spigot = ???
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.scheduler.BukkitTask
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param user May be null.
|
||||||
|
* @param channel May not be null.
|
||||||
|
*/
|
||||||
|
abstract class DiscordSenderBase protected(var user: User, var channel: MessageChannel) extends CommandSender {
|
||||||
|
private var msgtosend = ""
|
||||||
|
private var sendtask: BukkitTask = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user. May be null.
|
||||||
|
*
|
||||||
|
* @return The user or null.
|
||||||
|
*/
|
||||||
|
def getUser: User = user
|
||||||
|
|
||||||
|
def getChannel: MessageChannel = channel
|
||||||
|
|
||||||
|
private var chromaUser: DiscordPlayer = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the user data on first query.
|
||||||
|
*
|
||||||
|
* @return A Chroma user of Discord or a Discord user of Chroma
|
||||||
|
*/
|
||||||
|
def getChromaUser: DiscordPlayer = {
|
||||||
|
if (chromaUser == null) chromaUser = ChromaGamerBase.getUser(user.getId.asString, classOf[DiscordPlayer])
|
||||||
|
chromaUser
|
||||||
|
}
|
||||||
|
|
||||||
|
override def sendMessage(message: String): Unit = try {
|
||||||
|
val broadcast = new Exception().getStackTrace()(2).getMethodName.contains("broadcast")
|
||||||
|
if (broadcast) { //We're catching broadcasts using the Bukkit event
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
val sendmsg = DPUtils.sanitizeString(message)
|
||||||
|
this synchronized {
|
||||||
|
msgtosend += "\n" + sendmsg
|
||||||
|
if (sendtask == null) sendtask = Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, (() => {
|
||||||
|
channel.createMessage((if (user != null) user.getMention + "\n" else "") + msgtosend.trim).subscribe()
|
||||||
|
sendtask = null
|
||||||
|
msgtosend = ""
|
||||||
|
}): Runnable, 4) // Waits a 0.2 second to gather all/most of the different messages
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e, DiscordPlugin.plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def sendMessage(messages: Array[String]): Unit = sendMessage(String.join("\n", messages: _*))
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package buttondevteam.discordplugin
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.playerfaker.VCMDWrapper
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
trait IMCPlayer[T] extends Player {
|
||||||
|
def getVanillaCmdListener: VCMDWrapper
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package buttondevteam.discordplugin.announcer
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.{Component, ComponentMetadata}
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts).
|
||||||
|
*/
|
||||||
|
@ComponentMetadata(enabledByDefault = false) object AnnouncerModule {
|
||||||
|
private var stop = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentMetadata(enabledByDefault = false) class AnnouncerModule extends Component[DiscordPlugin] {
|
||||||
|
/**
|
||||||
|
* Channel to post new posts.
|
||||||
|
*/
|
||||||
|
final val channel = DPUtils.channelData(getConfig, "channel")
|
||||||
|
/**
|
||||||
|
* Channel where distinguished (moderator) posts go.
|
||||||
|
*/
|
||||||
|
final private val modChannel = DPUtils.channelData(getConfig, "modChannel")
|
||||||
|
/**
|
||||||
|
* Automatically unpins all messages except the last few. Set to 0 or >50 to disable
|
||||||
|
*/
|
||||||
|
final private val keepPinned = getConfig.getData("keepPinned", 40.toShort)
|
||||||
|
final private val lastAnnouncementTime = getConfig.getData("lastAnnouncementTime", 0L)
|
||||||
|
final private val lastSeenTime = getConfig.getData("lastSeenTime", 0L)
|
||||||
|
/**
|
||||||
|
* The subreddit to pull the posts from
|
||||||
|
*/
|
||||||
|
final private val subredditURL = getConfig.getData("subredditURL", "https://www.reddit.com/r/ChromaGamers")
|
||||||
|
|
||||||
|
override protected def enable(): Unit = {
|
||||||
|
if (DPUtils.disableIfConfigError(this, channel, modChannel)) return ()
|
||||||
|
AnnouncerModule.stop = false //If not the first time
|
||||||
|
val kp = keepPinned.get
|
||||||
|
if (kp <= 0) return ()
|
||||||
|
val msgs = channel.get.flatMapMany(_.getPinnedMessages).takeLast(kp)
|
||||||
|
msgs.subscribe(_.unpin)
|
||||||
|
new Thread(() => this.AnnouncementGetterThreadMethod()).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def disable(): Unit = AnnouncerModule.stop = true
|
||||||
|
|
||||||
|
@tailrec
|
||||||
|
private def AnnouncementGetterThreadMethod(): Unit = {
|
||||||
|
if (AnnouncerModule.stop) return ()
|
||||||
|
if (isEnabled) try { //If not enabled, just wait
|
||||||
|
val body = TBMCCoreAPI.DownloadString(subredditURL.get + "/new/.json?limit=10")
|
||||||
|
val json = new JsonParser().parse(body).getAsJsonObject.get("data").getAsJsonObject.get("children").getAsJsonArray
|
||||||
|
val msgsb = new StringBuilder
|
||||||
|
val modmsgsb = new StringBuilder
|
||||||
|
var lastanntime = lastAnnouncementTime.get
|
||||||
|
for (i <- json.size - 1 to 0 by -1) {
|
||||||
|
val item = json.get(i).getAsJsonObject
|
||||||
|
val data = item.get("data").getAsJsonObject
|
||||||
|
var author = data.get("author").getAsString
|
||||||
|
val distinguishedjson = data.get("distinguished")
|
||||||
|
val distinguished = if (distinguishedjson.isJsonNull) null else distinguishedjson.getAsString
|
||||||
|
val permalink = "https://www.reddit.com" + data.get("permalink").getAsString
|
||||||
|
val date = data.get("created_utc").getAsLong
|
||||||
|
if (date > lastSeenTime.get) lastSeenTime.set(date)
|
||||||
|
else if (date > lastAnnouncementTime.get) { //noinspection ConstantConditions
|
||||||
|
{
|
||||||
|
val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit")
|
||||||
|
if (reddituserclass != null) {
|
||||||
|
val user = ChromaGamerBase.getUser(author, reddituserclass)
|
||||||
|
val id = user.getConnectedID(classOf[DiscordPlayer])
|
||||||
|
if (id != null) author = "<@" + id + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!author.startsWith("<")) author = "/u/" + author
|
||||||
|
(if (distinguished != null && distinguished == "moderator") modmsgsb else msgsb)
|
||||||
|
.append("A new post was submitted to the subreddit by ").append(author).append("\n")
|
||||||
|
.append(permalink).append("\n")
|
||||||
|
lastanntime = date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendMsg(ch: SMono[MessageChannel], msg: String) =
|
||||||
|
ch.asJava().flatMap(c => c.createMessage(msg)).flatMap(_.pin).subscribe()
|
||||||
|
|
||||||
|
if (msgsb.nonEmpty) sendMsg(channel.get(), msgsb.toString())
|
||||||
|
if (modmsgsb.nonEmpty) sendMsg(modChannel.get(), modmsgsb.toString())
|
||||||
|
if (lastAnnouncementTime.get != lastanntime) lastAnnouncementTime.set(lastanntime) // If sending succeeded
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
try Thread.sleep(10000)
|
||||||
|
catch {
|
||||||
|
case ex: InterruptedException =>
|
||||||
|
Thread.currentThread.interrupt()
|
||||||
|
}
|
||||||
|
AnnouncementGetterThreadMethod()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package buttondevteam.discordplugin.broadcaster
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.{Component, ComponentMetadata}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone.
|
||||||
|
* If this component is enabled then these messages will show up on Discord.
|
||||||
|
*/
|
||||||
|
@ComponentMetadata(enabledByDefault = false) object GeneralEventBroadcasterModule {
|
||||||
|
def isHooked: Boolean = GeneralEventBroadcasterModule.hooked
|
||||||
|
|
||||||
|
private var hooked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentMetadata(enabledByDefault = false) class GeneralEventBroadcasterModule extends Component[DiscordPlugin] {
|
||||||
|
override protected def enable(): Unit = try {
|
||||||
|
PlayerListWatcher.hookUpDown(true, this)
|
||||||
|
log("Finished hooking into the player list")
|
||||||
|
GeneralEventBroadcasterModule.hooked = true
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Error while hacking the player list! Disable this module if you're on an incompatible version.", e, this)
|
||||||
|
case _: NoClassDefFoundError =>
|
||||||
|
logWarn("Error while hacking the player list! Disable this module if you're on an incompatible version.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def disable(): Unit = try {
|
||||||
|
if (!GeneralEventBroadcasterModule.hooked) return ()
|
||||||
|
if (PlayerListWatcher.hookUpDown(false, this)) log("Finished unhooking the player list!")
|
||||||
|
else log("Didn't have the player list hooked.")
|
||||||
|
GeneralEventBroadcasterModule.hooked = false
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Error while hacking the player list!", e, this)
|
||||||
|
case _: NoClassDefFoundError =>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package buttondevteam.discordplugin.broadcaster
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatUtils
|
||||||
|
import buttondevteam.discordplugin.playerfaker.DelegatingMockMaker
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker
|
||||||
|
import org.mockito.invocation.InvocationOnMock
|
||||||
|
import org.mockito.stubbing.Answer
|
||||||
|
|
||||||
|
import java.lang.invoke.{MethodHandle, MethodHandles}
|
||||||
|
import java.lang.reflect.{Constructor, Method, Modifier}
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object PlayerListWatcher {
|
||||||
|
private var plist: AnyRef = null
|
||||||
|
private var mock: AnyRef = null
|
||||||
|
private var fHandle: MethodHandle = null //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16
|
||||||
|
@throws[Exception]
|
||||||
|
private[broadcaster] def hookUpDown(up: Boolean, module: GeneralEventBroadcasterModule): Boolean = {
|
||||||
|
val csc = Bukkit.getServer.getClass
|
||||||
|
val conf = csc.getDeclaredField("console")
|
||||||
|
conf.setAccessible(true)
|
||||||
|
val server = conf.get(Bukkit.getServer)
|
||||||
|
val nms = server.getClass.getPackage.getName
|
||||||
|
val dplc = Class.forName(nms + ".DedicatedPlayerList")
|
||||||
|
val currentPL = server.getClass.getMethod("getPlayerList").invoke(server)
|
||||||
|
if (up) {
|
||||||
|
if (currentPL eq mock) {
|
||||||
|
module.logWarn("Player list already mocked!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
DelegatingMockMaker.getInstance.setMockMaker(new SubclassByteBuddyMockMaker)
|
||||||
|
val icbcl = Class.forName(nms + ".IChatBaseComponent")
|
||||||
|
var sendMessageTemp: Method = null
|
||||||
|
try sendMessageTemp = server.getClass.getMethod("sendMessage", icbcl, classOf[UUID])
|
||||||
|
catch {
|
||||||
|
case e: NoSuchMethodException =>
|
||||||
|
sendMessageTemp = server.getClass.getMethod("sendMessage", icbcl)
|
||||||
|
}
|
||||||
|
val sendMessageMethod = sendMessageTemp
|
||||||
|
val cmtcl = Class.forName(nms + ".ChatMessageType")
|
||||||
|
val systemType = cmtcl.getDeclaredField("SYSTEM").get(null)
|
||||||
|
val chatType = cmtcl.getDeclaredField("CHAT").get(null)
|
||||||
|
val obc = csc.getPackage.getName
|
||||||
|
val ccmcl = Class.forName(obc + ".util.CraftChatMessage")
|
||||||
|
val fixComponent = ccmcl.getMethod("fixComponent", icbcl)
|
||||||
|
val ppoc = Class.forName(nms + ".PacketPlayOutChat")
|
||||||
|
var ppocCTemp: Constructor[_] = null
|
||||||
|
try ppocCTemp = ppoc.getConstructor(icbcl, cmtcl, classOf[UUID])
|
||||||
|
catch {
|
||||||
|
case _: Exception =>
|
||||||
|
ppocCTemp = ppoc.getConstructor(icbcl, cmtcl)
|
||||||
|
}
|
||||||
|
val ppocC = ppocCTemp
|
||||||
|
val sendAllMethod = dplc.getMethod("sendAll", Class.forName(nms + ".Packet"))
|
||||||
|
var tpt: Method = null
|
||||||
|
try tpt = icbcl.getMethod("toPlainText")
|
||||||
|
catch {
|
||||||
|
case _: NoSuchMethodException =>
|
||||||
|
tpt = icbcl.getMethod("getString")
|
||||||
|
}
|
||||||
|
val toPlainText = tpt
|
||||||
|
val sysb = Class.forName(nms + ".SystemUtils").getField("b")
|
||||||
|
//Find the original method without overrides
|
||||||
|
var lookupConstructor: Constructor[MethodHandles.Lookup] = null
|
||||||
|
if (nms.contains("1_16")) {
|
||||||
|
lookupConstructor = classOf[MethodHandles.Lookup].getDeclaredConstructor(classOf[Class[_]])
|
||||||
|
lookupConstructor.setAccessible(true) //Create lookup with a given class instead of caller
|
||||||
|
}
|
||||||
|
else lookupConstructor = null
|
||||||
|
mock = Mockito.mock(dplc, Mockito.withSettings.defaultAnswer(new Answer[AnyRef]() { // Cannot call super constructor
|
||||||
|
@throws[Throwable]
|
||||||
|
override def answer(invocation: InvocationOnMock): AnyRef = {
|
||||||
|
val method = invocation.getMethod
|
||||||
|
if (!(method.getName == "sendMessage")) {
|
||||||
|
if (method.getName == "sendAll") {
|
||||||
|
sendAll(invocation.getArgument(0))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
//In 1.16 it passes a reference to the player list to advancement data for each player
|
||||||
|
if (nms.contains("1_16") && method.getName == "f" && method.getParameterCount > 0 && method.getParameterTypes()(0).getSimpleName == "EntityPlayer") {
|
||||||
|
method.setAccessible(true)
|
||||||
|
if (fHandle == null) {
|
||||||
|
assert(lookupConstructor != null)
|
||||||
|
val lookup = lookupConstructor.newInstance(mock.getClass)
|
||||||
|
fHandle = lookup.unreflectSpecial(method, mock.getClass) //Special: super.method()
|
||||||
|
}
|
||||||
|
return fHandle.invoke(mock, invocation.getArgument(0)) //Invoke with our instance, so it passes that to advancement data, we have the fields as well
|
||||||
|
}
|
||||||
|
return method.invoke(plist, invocation.getArguments)
|
||||||
|
}
|
||||||
|
val args = invocation.getArguments
|
||||||
|
val params = method.getParameterTypes
|
||||||
|
if (params.isEmpty) {
|
||||||
|
TBMCCoreAPI.SendException("Found a strange method", new Exception("Found a sendMessage() method without arguments."), module)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (params(0).getSimpleName == "IChatBaseComponent[]") for (arg <- args(0).asInstanceOf[Array[AnyRef]]) {
|
||||||
|
sendMessage(arg, system = true)
|
||||||
|
}
|
||||||
|
else if (params(0).getSimpleName == "IChatBaseComponent") if (params.length > 1 && params(1).getSimpleName.equalsIgnoreCase("boolean")) sendMessage(args(0), args(1).asInstanceOf[Boolean])
|
||||||
|
else sendMessage(args(0), system = true)
|
||||||
|
else TBMCCoreAPI.SendException("Found a method with interesting params", new Exception("Found a sendMessage(" + params(0).getSimpleName + ") method"), module)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sendMessage(chatComponent: Any, system: Boolean) = try { //Converted to use reflection
|
||||||
|
if (sendMessageMethod.getParameterCount == 2) sendMessageMethod.invoke(server, chatComponent, sysb.get(null))
|
||||||
|
else sendMessageMethod.invoke(server, chatComponent)
|
||||||
|
val chatmessagetype = if (system) systemType
|
||||||
|
else chatType
|
||||||
|
// CraftBukkit start - we run this through our processor first so we can get web links etc
|
||||||
|
val comp = fixComponent.invoke(null, chatComponent)
|
||||||
|
val packet = if (ppocC.getParameterCount == 3) ppocC.newInstance(comp, chatmessagetype, sysb.get(null))
|
||||||
|
else ppocC.newInstance(comp, chatmessagetype)
|
||||||
|
this.sendAll(packet)
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occurred while passing a vanilla message through the player list", e, module)
|
||||||
|
}
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sendAll(packet: Any) = try { // Some messages get sent by directly constructing a packet
|
||||||
|
sendAllMethod.invoke(plist, packet)
|
||||||
|
if (packet.getClass eq ppoc) {
|
||||||
|
val msgf = ppoc.getDeclaredField("a")
|
||||||
|
msgf.setAccessible(true)
|
||||||
|
MCChatUtils.forPublicPrivateChat(MCChatUtils.send(toPlainText.invoke(msgf.get(packet)).asInstanceOf[String])).subscribe()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e, module)
|
||||||
|
}
|
||||||
|
}).stubOnly).asInstanceOf
|
||||||
|
plist = currentPL
|
||||||
|
var plc = dplc
|
||||||
|
while ( {
|
||||||
|
plc != null
|
||||||
|
}) { //Set all fields
|
||||||
|
for (f <- plc.getDeclaredFields) {
|
||||||
|
f.setAccessible(true)
|
||||||
|
val modf = f.getClass.getDeclaredField("modifiers")
|
||||||
|
modf.setAccessible(true)
|
||||||
|
modf.set(f, f.getModifiers & ~Modifier.FINAL)
|
||||||
|
f.set(mock, f.get(plist))
|
||||||
|
}
|
||||||
|
plc = plc.getSuperclass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try server.getClass.getMethod("a", dplc).invoke(server, if (up) mock
|
||||||
|
else plist)
|
||||||
|
catch {
|
||||||
|
case e: NoSuchMethodException =>
|
||||||
|
server.getClass.getMethod("a", Class.forName(server.getClass.getPackage.getName + ".PlayerList")).invoke(server, if (up) mock
|
||||||
|
else plist)
|
||||||
|
}
|
||||||
|
val pllf = csc.getDeclaredField("playerList")
|
||||||
|
pllf.setAccessible(true)
|
||||||
|
pllf.set(Bukkit.getServer, if (up) mock
|
||||||
|
else plist)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.lib.chat.Command2
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.command.ApplicationCommandOption
|
||||||
|
import discord4j.discordjson.json.{ApplicationCommandOptionData, ApplicationCommandRequest}
|
||||||
|
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
class Command2DC extends Command2[ICommand2DC, Command2DCSender] {
|
||||||
|
override def registerCommand(command: ICommand2DC): Unit = {
|
||||||
|
registerCommand(command, DiscordPlugin.dc.getApplicationInfo.block().getId.asLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
def registerCommand(command: ICommand2DC, appId: Long, guildId: Option[Long] = None): Unit = {
|
||||||
|
super.registerCommand(command, DiscordPlugin.getPrefix) //Needs to be configurable for the helps
|
||||||
|
val greetCmdRequest = ApplicationCommandRequest.builder()
|
||||||
|
.name(command.getCommandPath) //TODO: Main path
|
||||||
|
.description("A ChromaBot command.") //TODO: Description
|
||||||
|
.addOption(ApplicationCommandOptionData.builder()
|
||||||
|
.name("name")
|
||||||
|
.description("Your name")
|
||||||
|
.`type`(ApplicationCommandOption.Type.STRING.getValue)
|
||||||
|
.required(true)
|
||||||
|
.build()
|
||||||
|
).build()
|
||||||
|
val service = DiscordPlugin.dc.getRestClient.getApplicationService
|
||||||
|
guildId match {
|
||||||
|
case Some(id) => service.createGuildApplicationCommand(appId, id, greetCmdRequest).subscribe()
|
||||||
|
case None => service.createGlobalApplicationCommand(appId, greetCmdRequest).subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def hasPermission(sender: Command2DCSender, command: ICommand2DC, method: Method): Boolean = {
|
||||||
|
//return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way?
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DPUtils
|
||||||
|
import buttondevteam.lib.chat.Command2Sender
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import discord4j.core.`object`.entity.{Message, User}
|
||||||
|
|
||||||
|
class Command2DCSender(val message: Message) extends Command2Sender {
|
||||||
|
def getMessage: Message = this.message
|
||||||
|
|
||||||
|
override def sendMessage(message: String): Unit = {
|
||||||
|
if (message.isEmpty) return ()
|
||||||
|
var msg = DPUtils.sanitizeString(message)
|
||||||
|
msg = Character.toLowerCase(message.charAt(0)) + message.substring(1)
|
||||||
|
this.message.getChannel.flatMap((ch: MessageChannel) => ch.createMessage(this.message.getAuthor.map((u: User) => DPUtils.nickMention(u.getId) + ", ").orElse("") + msg)).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def sendMessage(message: Array[String]): Unit = sendMessage(String.join("\n", message: _*))
|
||||||
|
|
||||||
|
override def getName: String = Option(message.getAuthor.orElse(null)).map(_.getUsername).getOrElse("Discord")
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlayer
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase}
|
||||||
|
import com.google.common.collect.HashBiMap
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Connect command", //
|
||||||
|
"This command lets you connect your account with a Minecraft account." +
|
||||||
|
" This allows using the private Minecraft chat and other things.")) object ConnectCommand {
|
||||||
|
/**
|
||||||
|
* Key: Minecraft name<br>
|
||||||
|
* Value: Discord ID
|
||||||
|
*/
|
||||||
|
var WaitingToConnect: HashBiMap[String, String] = HashBiMap.create
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Connect command",
|
||||||
|
"This command lets you connect your account with a Minecraft account." +
|
||||||
|
" This allows using the private Minecraft chat and other things.")) class ConnectCommand extends ICommand2DC {
|
||||||
|
@Command2.Subcommand def `def`(sender: Command2DCSender, Minecraftname: String): Boolean = {
|
||||||
|
val message = sender.getMessage
|
||||||
|
val channel = message.getChannel.block
|
||||||
|
val author = message.getAuthor.orElse(null)
|
||||||
|
if (author == null || channel == null) return true
|
||||||
|
if (ConnectCommand.WaitingToConnect.inverse.containsKey(author.getId.asString)) {
|
||||||
|
channel.createMessage("Replacing " + ConnectCommand.WaitingToConnect.inverse.get(author.getId.asString) + " with " + Minecraftname).subscribe()
|
||||||
|
ConnectCommand.WaitingToConnect.inverse.remove(author.getId.asString)
|
||||||
|
}
|
||||||
|
//noinspection ScalaDeprecation
|
||||||
|
val p = Bukkit.getOfflinePlayer(Minecraftname)
|
||||||
|
if (p == null) {
|
||||||
|
channel.createMessage("The specified Minecraft player cannot be found").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val pl = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer])
|
||||||
|
val dp = pl.getAs(classOf[DiscordPlayer])
|
||||||
|
if (dp != null && author.getId.asString == dp.getDiscordID) {
|
||||||
|
channel.createMessage("You already have this account connected.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ConnectCommand.WaitingToConnect.put(p.getName, author.getId.asString)
|
||||||
|
channel.createMessage("Alright! Now accept the connection in Minecraft from the account " + Minecraftname + " before the next server restart. You can also adjust the Minecraft name you want to connect to by running this command again.").subscribe()
|
||||||
|
if (p.isOnline) p.asInstanceOf[Player].sendMessage("§bTo connect with the Discord account " + author.getUsername + "#" + author.getDiscriminator + " do /discord accept")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.discordplugin.listeners.CommonListeners
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.{Member, User}
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Switches debug mode."))
|
||||||
|
class DebugCommand extends ICommand2DC {
|
||||||
|
@Command2.Subcommand
|
||||||
|
override def `def`(sender: Command2DCSender): Boolean = {
|
||||||
|
SMono(sender.getMessage.getAuthorAsMember)
|
||||||
|
.switchIfEmpty(Option(sender.getMessage.getAuthor.orElse(null)) //Support DMs
|
||||||
|
.map((u: User) => SMono(u.asMember(DiscordPlugin.mainServer.getId))).getOrElse(SMono.empty))
|
||||||
|
.flatMap((m: Member) => DiscordPlugin.plugin.modRole.get
|
||||||
|
.map(mr => m.getRoleIds.stream.anyMatch((r: Snowflake) => r == mr.getId))
|
||||||
|
.switchIfEmpty(SMono.fromCallable(() => DiscordPlugin.mainServer.getOwnerId.asLong == m.getId.asLong)))
|
||||||
|
.onErrorResume(_ => SMono.just(false)) //Role not found
|
||||||
|
.subscribe(success => {
|
||||||
|
if (success) {
|
||||||
|
CommonListeners.debug = !CommonListeners.debug;
|
||||||
|
sender.sendMessage("debug " + (if (CommonListeners.debug) "enabled" else "disabled"))
|
||||||
|
} else
|
||||||
|
sender.sendMessage("you need to be a moderator to use this command.")
|
||||||
|
})
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Help command", //
|
||||||
|
"Shows some info about a command or lists the available commands."))
|
||||||
|
class HelpCommand extends ICommand2DC {
|
||||||
|
@Command2.Subcommand
|
||||||
|
def `def`(sender: Command2DCSender, @Command2.TextArg @Command2.OptionalArg args: String): Boolean = {
|
||||||
|
if (args == null || args.isEmpty) sender.sendMessage(getManager.getCommandsText)
|
||||||
|
else {
|
||||||
|
val ht = getManager.getHelpText(args)
|
||||||
|
if (ht == null) sender.sendMessage("Command not found: " + args)
|
||||||
|
else sender.sendMessage(ht)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.lib.chat.{CommandClass, ICommand2}
|
||||||
|
|
||||||
|
abstract class ICommand2DC() extends ICommand2[Command2DCSender](DiscordPlugin.plugin.manager) {
|
||||||
|
final private var modOnly = false
|
||||||
|
|
||||||
|
{
|
||||||
|
val ann: CommandClass = getClass.getAnnotation(classOf[CommandClass])
|
||||||
|
if (ann == null) modOnly = false
|
||||||
|
else modOnly = ann.modOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
def isModOnly: Boolean = this.modOnly
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.{DiscordPlayer, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase.InfoTarget
|
||||||
|
import discord4j.core.`object`.entity.{Message, User}
|
||||||
|
import reactor.core.scala.publisher.SFlux
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters.ListHasAsScala
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("User information", //
|
||||||
|
"Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.",
|
||||||
|
"If used without args, shows your info."))
|
||||||
|
class UserinfoCommand extends ICommand2DC {
|
||||||
|
@Command2.Subcommand
|
||||||
|
def `def`(sender: Command2DCSender, @Command2.OptionalArg @Command2.TextArg user: String): Boolean = {
|
||||||
|
val message = sender.getMessage
|
||||||
|
var target: User = null
|
||||||
|
val channel = message.getChannel.block
|
||||||
|
assert(channel != null)
|
||||||
|
if (user == null || user.isEmpty) target = message.getAuthor.orElse(null)
|
||||||
|
else {
|
||||||
|
val firstmention = message.getUserMentions.asScala.find((m: User) => !(m.getId.asString == DiscordPlugin.dc.getSelfId.asString))
|
||||||
|
if (firstmention.isDefined) target = firstmention.get
|
||||||
|
else if (user.contains("#")) {
|
||||||
|
val targettag = user.split("#")
|
||||||
|
val targets = getUsers(message, targettag(0))
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
channel.createMessage("The user cannot be found (by name): " + user).subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
targets.collectFirst {
|
||||||
|
case user => user.getDiscriminator.equalsIgnoreCase(targettag(1))
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size + " users with the name.)").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val targets = getUsers(message, user)
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
channel.createMessage("The user cannot be found on Discord: " + user).subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (targets.size > 1) {
|
||||||
|
channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
target = targets.head
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage("An error occurred.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val dp = ChromaGamerBase.getUser(target.getId.asString, classOf[DiscordPlayer])
|
||||||
|
val uinfo = new StringBuilder("User info for ").append(target.getUsername).append(":\n")
|
||||||
|
uinfo.append(dp.getInfo(InfoTarget.Discord))
|
||||||
|
channel.createMessage(uinfo.toString).subscribe()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getUsers(message: Message, args: String) = {
|
||||||
|
val guild = message.getGuild.block
|
||||||
|
if (guild == null) { //Private channel
|
||||||
|
SFlux(DiscordPlugin.dc.getUsers).filter(u => u.getUsername.equalsIgnoreCase(args)).collectSeq().block()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
SFlux(guild.getMembers).filter(_.getUsername.equalsIgnoreCase(args)).map(_.asInstanceOf[User]).collectSeq().block()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package buttondevteam.discordplugin.commands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Version", "Returns the plugin's version"))
|
||||||
|
object VersionCommand {
|
||||||
|
def getVersion: Array[String] = {
|
||||||
|
val desc = DiscordPlugin.plugin.getDescription
|
||||||
|
Array[String](desc.getFullName, desc.getWebsite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array("Version", "Returns the plugin's version"))
|
||||||
|
class VersionCommand extends ICommand2DC {
|
||||||
|
@Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = {
|
||||||
|
sender.sendMessage(VersionCommand.getVersion)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package buttondevteam.discordplugin.exceptions
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.lib.TBMCDebugMessageEvent
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import org.bukkit.event.{EventHandler, Listener}
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
object DebugMessageListener {
|
||||||
|
private def SendMessage(message: String): Unit = {
|
||||||
|
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(classOf[ExceptionListenerModule])) return ()
|
||||||
|
try {
|
||||||
|
val mc = ExceptionListenerModule.getChannel
|
||||||
|
if (mc == null) return ()
|
||||||
|
val sb = new StringBuilder
|
||||||
|
sb.append("```").append("\n")
|
||||||
|
sb.append(if (message.length > 2000) message.substring(0, 2000) else message).append("\n")
|
||||||
|
sb.append("```")
|
||||||
|
mc.flatMap((ch: MessageChannel) => SMono(ch.createMessage(sb.toString))).subscribe()
|
||||||
|
} catch {
|
||||||
|
case ex: Exception =>
|
||||||
|
ex.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebugMessageListener extends Listener {
|
||||||
|
@EventHandler def onDebugMessage(e: TBMCDebugMessageEvent): Unit = {
|
||||||
|
DebugMessageListener.SendMessage(e.getDebugMessage)
|
||||||
|
e.setSent()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package buttondevteam.discordplugin.exceptions
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.architecture.Component
|
||||||
|
import buttondevteam.lib.{TBMCCoreAPI, TBMCExceptionEvent}
|
||||||
|
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
|
||||||
|
import discord4j.core.`object`.entity.{Guild, Role}
|
||||||
|
import org.apache.commons.lang.exception.ExceptionUtils
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.event.{EventHandler, Listener}
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy.
|
||||||
|
*/
|
||||||
|
object ExceptionListenerModule {
|
||||||
|
private def SendException(e: Throwable, sourcemessage: String): Unit = {
|
||||||
|
if (instance == null) return ()
|
||||||
|
try getChannel.flatMap(channel => {
|
||||||
|
val coderRole = channel match {
|
||||||
|
case ch: GuildChannel => instance.pingRole(SMono(ch.getGuild)).get
|
||||||
|
case _ => SMono.empty
|
||||||
|
}
|
||||||
|
coderRole.map((role: Role) => if (TBMCCoreAPI.IsTestServer) new StringBuilder
|
||||||
|
else new StringBuilder(role.getMention).append("\n"))
|
||||||
|
.defaultIfEmpty(new StringBuilder).flatMap(sb => {
|
||||||
|
sb.append(sourcemessage).append("\n")
|
||||||
|
sb.append("```").append("\n")
|
||||||
|
var stackTrace = util.Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n"))
|
||||||
|
.filter(s => !s.contains("\tat ") || s.contains("buttondevteam."))
|
||||||
|
.collect(Collectors.joining("\n"))
|
||||||
|
if (sb.length + stackTrace.length >= 1980) stackTrace = stackTrace.substring(0, 1980 - sb.length)
|
||||||
|
sb.append(stackTrace).append("\n")
|
||||||
|
sb.append("```")
|
||||||
|
SMono(channel.createMessage(sb.toString))
|
||||||
|
})
|
||||||
|
}).subscribe()
|
||||||
|
catch {
|
||||||
|
case ex: Exception =>
|
||||||
|
ex.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var instance: ExceptionListenerModule = null
|
||||||
|
|
||||||
|
def getChannel: SMono[MessageChannel] = {
|
||||||
|
if (instance != null) return instance.channel.get
|
||||||
|
SMono.empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExceptionListenerModule extends Component[DiscordPlugin] with Listener {
|
||||||
|
final private val lastthrown = new util.ArrayList[Throwable]
|
||||||
|
final private val lastsourcemsg = new util.ArrayList[String]
|
||||||
|
|
||||||
|
@EventHandler def onException(e: TBMCExceptionEvent): Unit = {
|
||||||
|
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass)) return ()
|
||||||
|
if (lastthrown.stream.anyMatch(ex => e.getException.getStackTrace.sameElements(ex.getStackTrace)
|
||||||
|
&& (if (e.getException.getMessage == null) ex.getMessage == null else e.getException.getMessage == ex.getMessage))
|
||||||
|
&& lastsourcemsg.contains(e.getSourceMessage)) {
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
ExceptionListenerModule.SendException(e.getException, e.getSourceMessage)
|
||||||
|
if (lastthrown.size >= 10) lastthrown.remove(0)
|
||||||
|
if (lastsourcemsg.size >= 10) lastsourcemsg.remove(0)
|
||||||
|
lastthrown.add(e.getException)
|
||||||
|
lastsourcemsg.add(e.getSourceMessage)
|
||||||
|
e.setHandled()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel to post the errors to.
|
||||||
|
*/
|
||||||
|
final private val channel = DPUtils.channelData(getConfig, "channel")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The role to ping if an error occurs. Set to empty ('') to disable.
|
||||||
|
*/
|
||||||
|
private def pingRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "pingRole", "Coder", guild)
|
||||||
|
|
||||||
|
override protected def enable(): Unit = {
|
||||||
|
if (DPUtils.disableIfConfigError(this, channel)) return ()
|
||||||
|
ExceptionListenerModule.instance = this
|
||||||
|
Bukkit.getPluginManager.registerEvents(new ExceptionListenerModule, getPlugin)
|
||||||
|
TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener, getPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def disable(): Unit = ExceptionListenerModule.instance = null
|
||||||
|
}
|
134
src/main/scala/buttondevteam/discordplugin/fun/FunModule.scala
Normal file
134
src/main/scala/buttondevteam/discordplugin/fun/FunModule.scala
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package buttondevteam.discordplugin.fun
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.{Component, ConfigData}
|
||||||
|
import com.google.common.collect.Lists
|
||||||
|
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
|
||||||
|
import discord4j.core.`object`.entity.{Guild, Message}
|
||||||
|
import discord4j.core.`object`.presence.Status
|
||||||
|
import discord4j.core.event.domain.PresenceUpdateEvent
|
||||||
|
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageCreateSpec}
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent
|
||||||
|
import org.bukkit.event.{EventHandler, Listener}
|
||||||
|
import reactor.core.scala.publisher.{SFlux, SMono}
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.stream.IntStream
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All kinds of random things.
|
||||||
|
* The YEEHAW event uses an emoji named :YEEHAW: if available
|
||||||
|
*/
|
||||||
|
object FunModule {
|
||||||
|
private val serverReadyStrings = Array[String]("in one week from now", // Ali
|
||||||
|
"between now and the heat-death of the universe.", // Ghostise
|
||||||
|
"soon™", "ask again this time next month", "in about 3 seconds", // Nicolai
|
||||||
|
"after we finish 8 plugins", "tomorrow.", "after one tiiiny feature",
|
||||||
|
"next commit", "after we finish strangling Towny", "when we kill every *fucking* bug",
|
||||||
|
"once the server stops screaming.", "after HL3 comes out", "next time you ask",
|
||||||
|
"when will *you* be open?") // Ali
|
||||||
|
private val serverReadyRandom = new Random
|
||||||
|
private val usableServerReadyStrings = new java.util.ArrayList[Short](0)
|
||||||
|
private var lastlist = 0
|
||||||
|
private var lastlistp = 0
|
||||||
|
private var ListC = 0
|
||||||
|
|
||||||
|
def executeMemes(message: Message): Boolean = {
|
||||||
|
val fm = ComponentManager.getIfEnabled(classOf[FunModule])
|
||||||
|
if (fm == null) return false
|
||||||
|
val msglowercased = message.getContent.toLowerCase
|
||||||
|
lastlist += 1
|
||||||
|
if (lastlist > 5) {
|
||||||
|
ListC = 0
|
||||||
|
lastlist = 0
|
||||||
|
}
|
||||||
|
if (msglowercased == "/list" && Bukkit.getOnlinePlayers.size == lastlistp && {
|
||||||
|
ListC += 1
|
||||||
|
ListC - 1
|
||||||
|
} > 2) { // Lowered already
|
||||||
|
DPUtils.reply(message, SMono.empty, "stop it. You know the answer.").subscribe()
|
||||||
|
lastlist = 0
|
||||||
|
lastlistp = Bukkit.getOnlinePlayers.size.toShort
|
||||||
|
return true //Handled
|
||||||
|
}
|
||||||
|
lastlistp = Bukkit.getOnlinePlayers.size.toShort //Didn't handle
|
||||||
|
if (!TBMCCoreAPI.IsTestServer && fm.serverReady.get.exists(msglowercased.contains)) {
|
||||||
|
var next = 0
|
||||||
|
if (usableServerReadyStrings.size == 0) fm.createUsableServerReadyStrings()
|
||||||
|
next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size))
|
||||||
|
DPUtils.reply(message, SMono.empty, fm.serverReadyAnswers.get.get(next)).subscribe()
|
||||||
|
return false //Still process it as a command/mcchat if needed
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lasttime: Long = 0
|
||||||
|
|
||||||
|
def handleFullHouse(event: PresenceUpdateEvent): Unit = {
|
||||||
|
val fm = ComponentManager.getIfEnabled(classOf[FunModule])
|
||||||
|
if (fm == null) return ()
|
||||||
|
if (Calendar.getInstance.get(Calendar.DAY_OF_MONTH) % 5 != 0) return ()
|
||||||
|
if (!Option(event.getOld.orElse(null)).exists(_.getStatus == Status.OFFLINE)
|
||||||
|
|| event.getCurrent.getStatus == Status.OFFLINE)
|
||||||
|
return () //If it's not an offline -> online change
|
||||||
|
fm.fullHouseChannel.get.filter((ch: MessageChannel) => ch.isInstanceOf[GuildChannel])
|
||||||
|
.flatMap(channel => fm.fullHouseDevRole(SMono(channel.asInstanceOf[GuildChannel].getGuild)).get
|
||||||
|
.filterWhen(devrole => SMono(event.getMember)
|
||||||
|
.flatMap(m => SFlux(m.getRoles).any(_.getId.asLong == devrole.getId.asLong)))
|
||||||
|
.filterWhen(devrole => SMono(event.getGuild)
|
||||||
|
.flatMapMany(g => SFlux(g.getMembers).filter(_.getRoleIds.stream.anyMatch(_ == devrole.getId)))
|
||||||
|
.flatMap(_.getPresence).all(_.getStatus != Status.OFFLINE))
|
||||||
|
.filter(_ => lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime)) //This should stay so it checks this last
|
||||||
|
.flatMap(_ => {
|
||||||
|
lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime)
|
||||||
|
SMono(channel.createMessage(_.setContent("Full house!")
|
||||||
|
.setEmbed((ecs: LegacyEmbedCreateSpec) => ecs.setImage("https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png"))))
|
||||||
|
})).subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunModule extends Component[DiscordPlugin] with Listener {
|
||||||
|
/**
|
||||||
|
* Questions that the bot will choose a random answer to give to.
|
||||||
|
*/
|
||||||
|
final private val serverReady: ConfigData[Array[String]] =
|
||||||
|
getConfig.getData("serverReady", () => Array[String](
|
||||||
|
"when will the server be open", "when will the server be ready",
|
||||||
|
"when will the server be done", "when will the server be complete",
|
||||||
|
"when will the server be finished", "when's the server ready",
|
||||||
|
"when's the server open", "vhen vill ze server be open?"))
|
||||||
|
/**
|
||||||
|
* Answers for a recognized question. Selected randomly.
|
||||||
|
*/
|
||||||
|
final private val serverReadyAnswers: ConfigData[util.ArrayList[String]] =
|
||||||
|
getConfig.getData("serverReadyAnswers", () => Lists.newArrayList(FunModule.serverReadyStrings: _*))
|
||||||
|
|
||||||
|
private def createUsableServerReadyStrings(): Unit =
|
||||||
|
IntStream.range(0, serverReadyAnswers.get.size).forEach((i: Int) => FunModule.usableServerReadyStrings.add(i.toShort))
|
||||||
|
|
||||||
|
override protected def enable(): Unit = registerListener(this)
|
||||||
|
|
||||||
|
override protected def disable(): Unit = {
|
||||||
|
FunModule.lastlist = 0
|
||||||
|
FunModule.lastlistp = 0
|
||||||
|
FunModule.ListC = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onPlayerJoin(event: PlayerJoinEvent): Unit = FunModule.ListC = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If all of the people who have this role are online, the bot will post a full house.
|
||||||
|
*/
|
||||||
|
private def fullHouseDevRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "fullHouseDevRole", "Developer", guild)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel to post the full house to.
|
||||||
|
*/
|
||||||
|
final private val fullHouseChannel = DPUtils.channelData(getConfig, "fullHouseChannel")
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package buttondevteam.discordplugin.listeners
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.fun.FunModule
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.role.GameRoleModule
|
||||||
|
import buttondevteam.discordplugin.util.Timings
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.architecture.Component
|
||||||
|
import discord4j.core.`object`.entity.Message
|
||||||
|
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
|
||||||
|
import discord4j.core.event.EventDispatcher
|
||||||
|
import discord4j.core.event.domain.PresenceUpdateEvent
|
||||||
|
import discord4j.core.event.domain.interaction.{ChatInputInteractionEvent, MessageInteractionEvent}
|
||||||
|
import discord4j.core.event.domain.message.MessageCreateEvent
|
||||||
|
import discord4j.core.event.domain.role.{RoleCreateEvent, RoleDeleteEvent, RoleUpdateEvent}
|
||||||
|
import reactor.core.Disposable
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scala.publisher.{SFlux, SMono}
|
||||||
|
|
||||||
|
object CommonListeners {
|
||||||
|
val timings = new Timings
|
||||||
|
|
||||||
|
def register(dispatcher: EventDispatcher): Unit = {
|
||||||
|
dispatcher.on(classOf[MessageCreateEvent]).flatMap((event: MessageCreateEvent) => {
|
||||||
|
SMono.just(event.getMessage).filter(_ => !DiscordPlugin.SafeMode)
|
||||||
|
.filter(message => message.getAuthor.filter(!_.isBot).isPresent)
|
||||||
|
.filter(message => !FunModule.executeMemes(message))
|
||||||
|
.filterWhen(message => {
|
||||||
|
Option(Component.getComponents.get(classOf[MinecraftChatModule])).filter(_.isEnabled)
|
||||||
|
.map(_.asInstanceOf[MinecraftChatModule].getListener.handleDiscord(event))
|
||||||
|
.getOrElse(SMono.just(true)) //Wasn't handled, continue
|
||||||
|
})
|
||||||
|
}).onErrorContinue((err, _) => TBMCCoreAPI.SendException("An error occured while handling a message!", err, DiscordPlugin.plugin)).subscribe()
|
||||||
|
dispatcher.on(classOf[PresenceUpdateEvent]).subscribe((event: PresenceUpdateEvent) => {
|
||||||
|
if (!DiscordPlugin.SafeMode)
|
||||||
|
FunModule.handleFullHouse(event)
|
||||||
|
})
|
||||||
|
SFlux(dispatcher.on(classOf[RoleCreateEvent])).subscribe(GameRoleModule.handleRoleEvent)
|
||||||
|
SFlux(dispatcher.on(classOf[RoleDeleteEvent])).subscribe(GameRoleModule.handleRoleEvent)
|
||||||
|
SFlux(dispatcher.on(classOf[RoleUpdateEvent])).subscribe(GameRoleModule.handleRoleEvent)
|
||||||
|
SFlux(dispatcher.on(classOf[ChatInputInteractionEvent], event => {
|
||||||
|
if(event.getCommandName() eq "help")
|
||||||
|
event.reply("Hello there")
|
||||||
|
else
|
||||||
|
Mono.empty()
|
||||||
|
})).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
var debug = false
|
||||||
|
|
||||||
|
def debug(debug: String): Unit = if (CommonListeners.debug) { //Debug
|
||||||
|
DPUtils.getLogger.info(debug)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package buttondevteam.discordplugin.listeners
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.commands.ConnectCommand
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.util.DPState
|
||||||
|
import buttondevteam.discordplugin.{DiscordPlayer, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.ScheduledServerRestartEvent
|
||||||
|
import buttondevteam.lib.player.TBMCPlayerGetInfoEvent
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent
|
||||||
|
import org.bukkit.event.{EventHandler, Listener}
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scala.publisher.javaOptional2ScalaOption
|
||||||
|
|
||||||
|
class MCListener extends Listener {
|
||||||
|
@EventHandler def onPlayerJoin(e: PlayerJoinEvent): Unit =
|
||||||
|
if (ConnectCommand.WaitingToConnect.containsKey(e.getPlayer.getName)) {
|
||||||
|
@SuppressWarnings(Array("ConstantConditions")) val user = DiscordPlugin.dc.getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.getPlayer.getName))).block
|
||||||
|
if (user == null) return ()
|
||||||
|
e.getPlayer.sendMessage("§bTo connect with the Discord account @" + user.getUsername + "#" + user.getDiscriminator + " do /discord accept")
|
||||||
|
e.getPlayer.sendMessage("§bIf it wasn't you, do /discord decline")
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onGetInfo(e: TBMCPlayerGetInfoEvent): Unit = {
|
||||||
|
Option(DiscordPlugin.SafeMode).filterNot(identity).flatMap(_ => Option(e.getPlayer.getAs(classOf[DiscordPlayer])))
|
||||||
|
.flatMap(dp => Option(dp.getDiscordID)).filter(_.nonEmpty)
|
||||||
|
.map(Snowflake.of).flatMap(id => DiscordPlugin.dc.getUserById(id).onErrorResume(_ => Mono.empty).blockOptional())
|
||||||
|
.map(user => {
|
||||||
|
e.addInfo("Discord tag: " + user.getUsername + "#" + user.getDiscriminator)
|
||||||
|
user
|
||||||
|
})
|
||||||
|
.flatMap(user => user.asMember(DiscordPlugin.mainServer.getId).onErrorResume(t => Mono.empty).blockOptional())
|
||||||
|
.flatMap(member => member.getPresence.blockOptional())
|
||||||
|
.map(pr => {
|
||||||
|
e.addInfo(pr.getStatus.toString)
|
||||||
|
pr
|
||||||
|
})
|
||||||
|
.flatMap(_.getActivity).foreach(activity => e.addInfo(s"${activity.getType}: ${activity.getName}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@EventHandler
|
||||||
|
public void onCommandPreprocess(TBMCCommandPreprocessEvent e) {
|
||||||
|
if (e.getMessage().equalsIgnoreCase("/stop"))
|
||||||
|
MinecraftChatModule.state = DPState.STOPPING_SERVER;
|
||||||
|
else if (e.getMessage().equalsIgnoreCase("/restart"))
|
||||||
|
MinecraftChatModule.state = DPState.RESTARTING_SERVER;
|
||||||
|
}*/ @EventHandler //We don't really need this with the logger stuff but hey
|
||||||
|
def onScheduledRestart(e: ScheduledServerRestartEvent): Unit =
|
||||||
|
MinecraftChatModule.state = DPState.RESTARTING_SERVER
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.component.channel.{Channel, ChatRoom}
|
||||||
|
import buttondevteam.discordplugin.*
|
||||||
|
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
|
||||||
|
import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC}
|
||||||
|
import buttondevteam.lib.TBMCSystemChatEvent
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer}
|
||||||
|
import discord4j.core.`object`.entity.Message
|
||||||
|
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
|
||||||
|
import discord4j.rest.util.{Permission, PermissionSet}
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util
|
||||||
|
import java.util.function.Supplier
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import java.util.{Objects, Optional}
|
||||||
|
import javax.annotation.Nullable
|
||||||
|
|
||||||
|
@SuppressWarnings(Array("SimplifyOptionalCallChains")) //Java 11
|
||||||
|
@CommandClass(helpText = Array("Channel connect", //
|
||||||
|
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).",
|
||||||
|
"You need to have access to the MC channel and have manage permissions on the Discord channel.",
|
||||||
|
"You also need to have your Minecraft account connected. In #bot use /connect <mcname>.",
|
||||||
|
"Call this command from the channel you want to use.", "Usage: @Bot channelcon <mcchannel>",
|
||||||
|
"Use the ID (command) of the channel, for example `g` for the global chat.",
|
||||||
|
"To remove a connection use @ChromaBot channelcon remove in the channel.",
|
||||||
|
"Mentioning the bot is needed in this case because the / prefix only works in #bot.",
|
||||||
|
"Invite link: <Unknown>" //
|
||||||
|
))
|
||||||
|
class ChannelconCommand(private val module: MinecraftChatModule) extends ICommand2DC {
|
||||||
|
@Command2.Subcommand def remove(sender: Command2DCSender): Boolean = {
|
||||||
|
val message = sender.getMessage
|
||||||
|
if (checkPerms(message, null)) return true
|
||||||
|
else if (MCChatCustom.removeCustomChat(message.getChannelId))
|
||||||
|
DPUtils.reply(message, SMono.empty, "channel connection removed.").subscribe()
|
||||||
|
else
|
||||||
|
DPUtils.reply(message, SMono.empty, "this channel isn't connected.").subscribe()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand def toggle(sender: Command2DCSender, @Command2.OptionalArg toggle: String): Boolean = {
|
||||||
|
val message = sender.getMessage
|
||||||
|
if (checkPerms(message, null)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val cc: MCChatCustom.CustomLMD = MCChatCustom.getCustomChat(message.getChannelId)
|
||||||
|
if (cc == null) {
|
||||||
|
return respond(sender, "this channel isn't connected.")
|
||||||
|
}
|
||||||
|
val togglesString: Supplier[String] = () => ChannelconBroadcast.values
|
||||||
|
.map(t => t.toString.toLowerCase + ": " + (if ((cc.toggles & (1 << t.id)) == 0) "disabled" else "enabled"))
|
||||||
|
.mkString("\n") + "\n\n" +
|
||||||
|
TBMCSystemChatEvent.BroadcastTarget.stream.map((target: TBMCSystemChatEvent.BroadcastTarget) =>
|
||||||
|
target.getName + ": " + (if (cc.brtoggles.contains(target)) "enabled" else "disabled"))
|
||||||
|
.collect(Collectors.joining("\n"))
|
||||||
|
if (toggle == null) {
|
||||||
|
DPUtils.reply(message, SMono.empty, "toggles:\n" + togglesString.get).subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val arg: String = toggle.toUpperCase
|
||||||
|
val b = ChannelconBroadcast.values.find((t: ChannelconBroadcast) => t.toString == arg)
|
||||||
|
if (b.isEmpty) {
|
||||||
|
val bt: TBMCSystemChatEvent.BroadcastTarget = TBMCSystemChatEvent.BroadcastTarget.get(arg)
|
||||||
|
if (bt == null) {
|
||||||
|
DPUtils.reply(message, SMono.empty, "cannot find toggle. Toggles:\n" + togglesString.get).subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val add: Boolean = !(cc.brtoggles.contains(bt))
|
||||||
|
if (add) {
|
||||||
|
cc.brtoggles += bt
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cc.brtoggles -= bt
|
||||||
|
}
|
||||||
|
return respond(sender, "'" + bt.getName + "' " + (if (add) "en" else "dis") + "abled")
|
||||||
|
}
|
||||||
|
//A B | F
|
||||||
|
//------- A: original - B: mask - F: new
|
||||||
|
//0 0 | 0
|
||||||
|
//0 1 | 1
|
||||||
|
//1 0 | 1
|
||||||
|
//1 1 | 0
|
||||||
|
// XOR
|
||||||
|
cc.toggles ^= (1 << b.get.id)
|
||||||
|
DPUtils.reply(message, SMono.empty, "'" + b.get.toString.toLowerCase + "' "
|
||||||
|
+ (if ((cc.toggles & (1 << b.get.id)) == 0) "disabled" else "enabled")).subscribe()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand def `def`(sender: Command2DCSender, channelID: String): Boolean = {
|
||||||
|
val message = sender.getMessage
|
||||||
|
if (!(module.allowCustomChat.get)) {
|
||||||
|
sender.sendMessage("channel connection is not allowed on this Minecraft server.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val channel = message.getChannel.block
|
||||||
|
if (checkPerms(message, channel)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (MCChatCustom.hasCustomChat(message.getChannelId)) {
|
||||||
|
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.")
|
||||||
|
}
|
||||||
|
val chan: Optional[Channel] = Channel.getChannels.filter((ch: Channel) => ch.ID.equalsIgnoreCase(channelID) || (util.Arrays.stream(ch.IDs.get).anyMatch((cid: String) => cid.equalsIgnoreCase(channelID)))).findAny
|
||||||
|
if (!(chan.isPresent)) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
|
||||||
|
DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!(message.getAuthor.isPresent)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val author = message.getAuthor.get
|
||||||
|
val dp: DiscordPlayer = ChromaGamerBase.getUser(author.getId.asString, classOf[DiscordPlayer])
|
||||||
|
val chp: TBMCPlayer = dp.getAs(classOf[TBMCPlayer])
|
||||||
|
if (chp == null) {
|
||||||
|
DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention + " do " + DiscordPlugin.getPrefix + "connect <MCname>").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val dcp: DiscordConnectedPlayer = DiscordConnectedPlayer.create(message.getAuthor.get, channel, chp.getUUID, Bukkit.getOfflinePlayer(chp.getUUID).getName, module)
|
||||||
|
//Using a fake player with no login/logout, should be fine for this event
|
||||||
|
val groupid: String = chan.get.getGroupID(dcp)
|
||||||
|
if (groupid == null && !((chan.get.isInstanceOf[ChatRoom]))) { //ChatRooms don't allow it unless the user joins, which happens later
|
||||||
|
DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (chan.get.isInstanceOf[ChatRoom]) { //ChatRooms don't work well
|
||||||
|
DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
|
||||||
|
DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm.");
|
||||||
|
return true;
|
||||||
|
}*/
|
||||||
|
//TODO: "Channel admins" that can connect channels?
|
||||||
|
MCChatCustom.addCustomChat(channel, groupid, chan.get, author, dcp, 0, Set())
|
||||||
|
if (chan.get.isInstanceOf[ChatRoom]) {
|
||||||
|
DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Array("ConstantConditions"))
|
||||||
|
private def checkPerms(message: Message, @Nullable channel: MessageChannel): Boolean = {
|
||||||
|
if (channel == null) {
|
||||||
|
return checkPerms(message, message.getChannel.block)
|
||||||
|
}
|
||||||
|
if (!((channel.isInstanceOf[GuildChannel]))) {
|
||||||
|
DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
//noinspection OptionalGetWithoutIsPresent
|
||||||
|
val perms: PermissionSet = (channel.asInstanceOf[GuildChannel]).getEffectivePermissions(message.getAuthor.map(_.getId).get).block
|
||||||
|
if (!(perms.contains(Permission.ADMINISTRATOR)) && !(perms.contains(Permission.MANAGE_CHANNELS))) {
|
||||||
|
DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] =
|
||||||
|
Array[String](
|
||||||
|
"Channel connect",
|
||||||
|
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).",
|
||||||
|
"You need to have access to the MC channel and have manage permissions on the Discord channel.",
|
||||||
|
"You also need to have your Minecraft account connected. In " + DPUtils.botmention + " use " + DiscordPlugin.getPrefix + "connect <mcname>.",
|
||||||
|
"Call this command from the channel you want to use.", "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf.block).getMention + " channelcon <mcchannel>",
|
||||||
|
"Use the ID (command) of the channel, for example `g` for the global chat.",
|
||||||
|
"To remove a connection use @ChromaBot channelcon remove in the channel.",
|
||||||
|
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix + " prefix only works in " + DPUtils.botmention + ".",
|
||||||
|
"Invite link: <https://discordapp.com/oauth2/authorize?client_id="
|
||||||
|
+ SMono(DiscordPlugin.dc.getApplicationInfo).map(info => info.getId.asString).blockOption().getOrElse("Unknown")
|
||||||
|
+ "&scope=bot&permissions=268509264>")
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC}
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import buttondevteam.lib.player.ChromaGamerBase
|
||||||
|
import discord4j.core.`object`.entity.channel.PrivateChannel
|
||||||
|
|
||||||
|
@CommandClass(helpText = Array(
|
||||||
|
"MC Chat",
|
||||||
|
"This command enables or disables the Minecraft chat in private messages.", //
|
||||||
|
"It can be useful if you don't want your messages to be visible, for example when talking in a private channel.",
|
||||||
|
"You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." //
|
||||||
|
))
|
||||||
|
class MCChatCommand(private val module: MinecraftChatModule) extends ICommand2DC {
|
||||||
|
@Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = {
|
||||||
|
if (!module.allowPrivateChat.get) {
|
||||||
|
sender.sendMessage("using the private chat is not allowed on this Minecraft server.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val message = sender.getMessage
|
||||||
|
val channel = message.getChannel.block
|
||||||
|
@SuppressWarnings(Array("OptionalGetWithoutIsPresent")) val author = message.getAuthor.get
|
||||||
|
if (!channel.isInstanceOf[PrivateChannel]) {
|
||||||
|
DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val user: DiscordPlayer = ChromaGamerBase.getUser(author.getId.asString, classOf[DiscordPlayer])
|
||||||
|
val mcchat: Boolean = !(user.isMinecraftChatEnabled)
|
||||||
|
MCChatPrivate.privateMCChat(channel, mcchat, author, user)
|
||||||
|
DPUtils.reply(message, channel, "Minecraft chat " +
|
||||||
|
(if (mcchat) "enabled. Use '" + DiscordPlugin.getPrefix + "mcchat' again to turn it off."
|
||||||
|
else "disabled.")).subscribe()
|
||||||
|
true
|
||||||
|
// TODO: Pin channel switching to indicate the current channel
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.component.channel.{Channel, ChatRoom}
|
||||||
|
import buttondevteam.discordplugin.DiscordConnectedPlayer
|
||||||
|
import buttondevteam.lib.TBMCSystemChatEvent
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
|
||||||
|
import javax.annotation.Nullable
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
object MCChatCustom {
|
||||||
|
/**
|
||||||
|
* Used for town or nation chats or anything else
|
||||||
|
*/
|
||||||
|
private[mcchat] val lastmsgCustom = new ListBuffer[MCChatCustom.CustomLMD]
|
||||||
|
|
||||||
|
def addCustomChat(channel: MessageChannel, groupid: String, mcchannel: Channel, user: User, dcp: DiscordConnectedPlayer, toggles: Int, brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]): Boolean = {
|
||||||
|
lastmsgCustom synchronized {
|
||||||
|
var gid: String = null
|
||||||
|
mcchannel match {
|
||||||
|
case room: ChatRoom =>
|
||||||
|
room.joinRoom(dcp)
|
||||||
|
gid = if (groupid == null) mcchannel.getGroupID(dcp) else groupid
|
||||||
|
case _ =>
|
||||||
|
gid = groupid
|
||||||
|
}
|
||||||
|
val lmd = new MCChatCustom.CustomLMD(channel, user, gid, mcchannel, dcp, toggles, brtoggles)
|
||||||
|
lastmsgCustom += lmd
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
def hasCustomChat(channel: Snowflake): Boolean =
|
||||||
|
lastmsgCustom.exists(_.channel.getId.asLong == channel.asLong)
|
||||||
|
|
||||||
|
@Nullable def getCustomChat(channel: Snowflake): CustomLMD =
|
||||||
|
lastmsgCustom.find(_.channel.getId.asLong == channel.asLong).orNull
|
||||||
|
|
||||||
|
def removeCustomChat(channel: Snowflake): Boolean = {
|
||||||
|
lastmsgCustom synchronized {
|
||||||
|
MCChatUtils.lastmsgfromd.remove(channel.asLong)
|
||||||
|
val count = lastmsgCustom.size
|
||||||
|
lastmsgCustom.filterInPlace(lmd => {
|
||||||
|
if (lmd.channel.getId.asLong != channel.asLong) true
|
||||||
|
else {
|
||||||
|
lmd.mcchannel match {
|
||||||
|
case room: ChatRoom => room.leaveRoom(lmd.dcp)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lastmsgCustom.size < count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getCustomChats: List[CustomLMD] = lastmsgCustom.toList
|
||||||
|
|
||||||
|
class CustomLMD private[mcchat](channel: MessageChannel, user: User, val groupID: String,
|
||||||
|
mcchannel: Channel, val dcp: DiscordConnectedPlayer, var toggles: Int,
|
||||||
|
var brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]) extends MCChatUtils.LastMsgData(channel, user, mcchannel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,474 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.*
|
||||||
|
import buttondevteam.discordplugin.DPUtils.SpecExtensions
|
||||||
|
import buttondevteam.discordplugin.playerfaker.{VanillaCommandListener, VanillaCommandListener14, VanillaCommandListener15}
|
||||||
|
import buttondevteam.lib.*
|
||||||
|
import buttondevteam.lib.chat.{ChatMessage, TBMCChatAPI}
|
||||||
|
import buttondevteam.lib.player.TBMCPlayer
|
||||||
|
import com.vdurmont.emoji.EmojiParser
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
|
||||||
|
import discord4j.core.`object`.entity.{Member, Message, User}
|
||||||
|
import discord4j.core.event.domain.message.MessageCreateEvent
|
||||||
|
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageEditSpec}
|
||||||
|
import discord4j.rest.util.Color
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.{EventHandler, Listener}
|
||||||
|
import org.bukkit.scheduler.BukkitTask
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scala.publisher.{SFlux, SMono}
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util
|
||||||
|
import java.util.concurrent.{LinkedBlockingQueue, TimeoutException}
|
||||||
|
import java.util.function.{Consumer, Predicate}
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import scala.jdk.CollectionConverters.{ListHasAsScala, SetHasAsScala}
|
||||||
|
import scala.jdk.OptionConverters.RichOptional
|
||||||
|
|
||||||
|
object MCChatListener {
|
||||||
|
|
||||||
|
// ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender
|
||||||
|
// Offline public chat......x............................................
|
||||||
|
// Online public chat.......x...........................................x
|
||||||
|
// Offline private chat.....x.......................x....................
|
||||||
|
// Online private chat......x.......................x...................x
|
||||||
|
// If online and enabling private chat, don't login
|
||||||
|
// If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority
|
||||||
|
// If private chat is enabled and joining the server, logout the fake player on highest priority
|
||||||
|
// If online and disabling private chat, don't logout
|
||||||
|
// The maps may not contain the senders for UnconnectedSenders
|
||||||
|
@FunctionalInterface private trait InterruptibleConsumer[T] {
|
||||||
|
@throws[TimeoutException]
|
||||||
|
@throws[InterruptedException]
|
||||||
|
def accept(value: T): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCChatListener(val module: MinecraftChatModule) extends Listener {
|
||||||
|
private var sendtask: BukkitTask = null
|
||||||
|
final private val sendevents = new LinkedBlockingQueue[util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant]]
|
||||||
|
private var sendrunnable: Runnable = null
|
||||||
|
private var sendthread: Thread = null
|
||||||
|
private var stop = false //A new instance will be created on enable
|
||||||
|
@EventHandler // Minecraft
|
||||||
|
def onMCChat(ev: TBMCChatEvent): Unit = {
|
||||||
|
if (!(ComponentManager.isEnabled(classOf[MinecraftChatModule])) || ev.isCancelled) { //SafeMode: Needed so it doesn't restart after server shutdown
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendevents.add(new util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant](ev, Instant.now))
|
||||||
|
if (sendtask != null) {
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
sendrunnable = () => {
|
||||||
|
def foo(): Unit = {
|
||||||
|
sendthread = Thread.currentThread
|
||||||
|
processMCToDiscord()
|
||||||
|
if (DiscordPlugin.plugin.isEnabled && !(stop)) { //Don't run again if shutting down
|
||||||
|
sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo()
|
||||||
|
}
|
||||||
|
sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def processMCToDiscord(): Unit = {
|
||||||
|
try {
|
||||||
|
var e: TBMCChatEvent = null
|
||||||
|
var time: Instant = null
|
||||||
|
val se: util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant] = sendevents.take // Wait until an element is available
|
||||||
|
e = se.getKey
|
||||||
|
time = se.getValue
|
||||||
|
val authorPlayer: String = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel.DisplayName.get) + "] " + //
|
||||||
|
(if ("Minecraft" == e.getOrigin) "" else "[" + e.getOrigin.charAt(0) + "]") +
|
||||||
|
DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender))
|
||||||
|
val color: chat.Color = e.getChannel.Color.get
|
||||||
|
val embed: Consumer[LegacyEmbedCreateSpec] = (ecs: LegacyEmbedCreateSpec) => {
|
||||||
|
def foo(ecs: LegacyEmbedCreateSpec) = {
|
||||||
|
ecs.setDescription(e.getMessage).setColor(Color.of(color.getRed, color.getGreen, color.getBlue))
|
||||||
|
val url: String = module.profileURL.get
|
||||||
|
e.getSender match {
|
||||||
|
case player: Player =>
|
||||||
|
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender.getName,
|
||||||
|
if (url.nonEmpty) url + "?type=minecraft&id=" + player.getUniqueId else null)
|
||||||
|
case dsender: DiscordSenderBase =>
|
||||||
|
ecs.setAuthor(authorPlayer,
|
||||||
|
if (url.nonEmpty) url + "?type=discord&id=" + dsender.getUser.getId.asString else null,
|
||||||
|
dsender.getUser.getAvatarUrl)
|
||||||
|
case _ =>
|
||||||
|
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender.getName, null)
|
||||||
|
}
|
||||||
|
ecs.setTimestamp(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(ecs)
|
||||||
|
}
|
||||||
|
val nanoTime: Long = System.nanoTime
|
||||||
|
val doit = (lastmsgdata: MCChatUtils.LastMsgData) => {
|
||||||
|
if (lastmsgdata.message == null
|
||||||
|
|| authorPlayer != lastmsgdata.message.getEmbeds.get(0).getAuthor.toScala.flatMap(_.getName.toScala).orNull
|
||||||
|
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|
||||||
|
|| !(lastmsgdata.mcchannel.ID == e.getChannel.ID)
|
||||||
|
|| lastmsgdata.content.length + e.getMessage.length + 1 > 2048) {
|
||||||
|
lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block
|
||||||
|
lastmsgdata.time = nanoTime
|
||||||
|
lastmsgdata.mcchannel = e.getChannel
|
||||||
|
lastmsgdata.content = e.getMessage
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lastmsgdata.content = lastmsgdata.content + "\n" + e.getMessage // The message object doesn't get updated
|
||||||
|
lastmsgdata.message.edit((mes: LegacyMessageEditSpec) => mes.setEmbed(embed.andThen((ecs: LegacyEmbedCreateSpec) => ecs.setDescription(lastmsgdata.content))).^^()).block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Checks if the given channel is different than where the message was sent from
|
||||||
|
// Or if it was from MC
|
||||||
|
val isdifferentchannel: Predicate[Snowflake] = (id: Snowflake) => !((e.getSender.isInstanceOf[DiscordSenderBase])) || (e.getSender.asInstanceOf[DiscordSenderBase]).getChannel.getId.asLong != id.asLong
|
||||||
|
if (e.getChannel.isGlobal && (e.isFromCommand || isdifferentchannel.test(module.chatChannel.get))) {
|
||||||
|
if (MCChatUtils.lastmsgdata == null)
|
||||||
|
MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null)
|
||||||
|
doit(MCChatUtils.lastmsgdata)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (data <- MCChatPrivate.lastmsgPerUser) {
|
||||||
|
if ((e.isFromCommand || isdifferentchannel.test(data.channel.getId)) && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId, data.user))) {
|
||||||
|
doit(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MCChatCustom.lastmsgCustom synchronized {
|
||||||
|
MCChatCustom.lastmsgCustom.filterInPlace(lmd => {
|
||||||
|
if ((e.isFromCommand || isdifferentchannel.test(lmd.channel.getId)) //Test if msg is from Discord
|
||||||
|
&& e.getChannel.ID == lmd.mcchannel.ID //If it's from a command, the command msg has been deleted, so we need to send it
|
||||||
|
&& e.getGroupID() == lmd.groupID) { //Check if this is the group we want to test - #58
|
||||||
|
if (e.shouldSendTo(lmd.dcp)) { //Check original user's permissions
|
||||||
|
doit(lmd)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe()
|
||||||
|
false //If the user no longer has permission, remove the connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case ex: InterruptedException =>
|
||||||
|
//Stop if interrupted anywhere
|
||||||
|
sendtask.cancel()
|
||||||
|
sendtask = null
|
||||||
|
case ex: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onChatPreprocess(event: TBMCChatPreprocessEvent): Unit = {
|
||||||
|
var start: Int = -(1)
|
||||||
|
while ( {
|
||||||
|
(start = event.getMessage.indexOf('@', start + 1), start) != ((), -1)
|
||||||
|
}) {
|
||||||
|
val mid: Int = event.getMessage.indexOf('#', start + 1)
|
||||||
|
if (mid == -1) {
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
var end_ = event.getMessage.indexOf(' ', mid + 1)
|
||||||
|
if (end_ == -1) {
|
||||||
|
end_ = event.getMessage.length
|
||||||
|
}
|
||||||
|
val end: Int = end_
|
||||||
|
val startF: Int = start
|
||||||
|
val user = DiscordPlugin.dc.getUsers.filter((u) => u.getUsername.equals(event.getMessage.substring(startF + 1, mid))).filter((u) => u.getDiscriminator.equals(event.getMessage.substring(mid + 1, end))).blockFirst
|
||||||
|
if (user != null) { //TODO: Nicknames
|
||||||
|
event.setMessage(event.getMessage.substring(0, startF) + "@" + user.getUsername + (if (event.getMessage.length > end) {
|
||||||
|
event.getMessage.substring(end)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
""
|
||||||
|
})) // TODO: Add formatting
|
||||||
|
}
|
||||||
|
start = end // Skip any @s inside the mention
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the listener permanently. Enabling the module will create a new instance.
|
||||||
|
*
|
||||||
|
* @param wait Wait 5 seconds for the threads to stop
|
||||||
|
*/
|
||||||
|
def stop(wait: Boolean): Unit = {
|
||||||
|
stop = true
|
||||||
|
MCChatPrivate.logoutAll()
|
||||||
|
MCChatUtils.LoggedInPlayers.clear()
|
||||||
|
if (sendthread != null) {
|
||||||
|
sendthread.interrupt()
|
||||||
|
}
|
||||||
|
if (recthread != null) {
|
||||||
|
recthread.interrupt()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (sendthread != null) {
|
||||||
|
sendthread.interrupt()
|
||||||
|
if (wait) {
|
||||||
|
sendthread.join(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (recthread != null) {
|
||||||
|
recthread.interrupt()
|
||||||
|
if (wait) {
|
||||||
|
recthread.join(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MCChatUtils.lastmsgdata = null
|
||||||
|
MCChatPrivate.lastmsgPerUser.clear()
|
||||||
|
MCChatCustom.lastmsgCustom.clear()
|
||||||
|
MCChatUtils.lastmsgfromd.clear()
|
||||||
|
MCChatUtils.UnconnectedSenders.clear()
|
||||||
|
recthread = null
|
||||||
|
sendthread = null
|
||||||
|
} catch {
|
||||||
|
case e: InterruptedException =>
|
||||||
|
e.printStackTrace() //This thread shouldn't be interrupted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rectask: BukkitTask = null
|
||||||
|
final private val recevents: LinkedBlockingQueue[MessageCreateEvent] = new LinkedBlockingQueue[MessageCreateEvent]
|
||||||
|
private var recrun: Runnable = null
|
||||||
|
private var recthread: Thread = null
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
def handleDiscord(ev: MessageCreateEvent): SMono[Boolean] = {
|
||||||
|
val author = Option(ev.getMessage.getAuthor.orElse(null))
|
||||||
|
val hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage.getChannelId)
|
||||||
|
val prefix = DiscordPlugin.getPrefix
|
||||||
|
SMono(ev.getMessage.getChannel)
|
||||||
|
.filter(channel => isChatEnabled(channel, author, hasCustomChat))
|
||||||
|
.filter(channel => !isRunningMCChatCommand(channel, ev.getMessage.getContent, prefix))
|
||||||
|
.filter(channel => {
|
||||||
|
MCChatUtils.resetLastMessage(channel)
|
||||||
|
recevents.add(ev)
|
||||||
|
if (rectask == null) {
|
||||||
|
recrun = () => {
|
||||||
|
recthread = Thread.currentThread
|
||||||
|
processDiscordToMC()
|
||||||
|
if (DiscordPlugin.plugin.isEnabled && !(stop)) {
|
||||||
|
rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Continue message processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Start message processing
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}).map(_ => false).defaultIfEmpty(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def isChatEnabled(channel: MessageChannel, author: Option[User], hasCustomChat: Boolean) = {
|
||||||
|
def hasPrivateChat = channel.isInstanceOf[PrivateChannel] &&
|
||||||
|
author.exists((u: User) => MCChatPrivate.isMinecraftChatEnabled(u.getId.asString))
|
||||||
|
|
||||||
|
def hasPublicChat = channel.getId.asLong == module.chatChannel.get.asLong
|
||||||
|
|
||||||
|
hasPublicChat || hasPrivateChat || hasCustomChat
|
||||||
|
}
|
||||||
|
|
||||||
|
private def isRunningMCChatCommand(channel: MessageChannel, content: String, prefix: Char) = {
|
||||||
|
(channel.isInstanceOf[PrivateChannel] //Only in private chat
|
||||||
|
&& content.length < "/mcchat<>".length
|
||||||
|
&& content.replace(prefix + "", "").equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat
|
||||||
|
//Allow disabling the chat if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
private def processDiscordToMC(): Unit = {
|
||||||
|
var event: MessageCreateEvent = null
|
||||||
|
try event = recevents.take
|
||||||
|
catch {
|
||||||
|
case _: InterruptedException =>
|
||||||
|
rectask.cancel()
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
val sender: User = event.getMessage.getAuthor.orElse(null)
|
||||||
|
var dmessage: String = event.getMessage.getContent
|
||||||
|
try {
|
||||||
|
val dsender: DiscordSenderBase = MCChatUtils.getSender(event.getMessage.getChannelId, sender)
|
||||||
|
val user: DiscordPlayer = dsender.getChromaUser
|
||||||
|
|
||||||
|
def replaceUserMentions(): Unit = {
|
||||||
|
for (u <- event.getMessage.getUserMentions.asScala) { //TODO: Role mentions
|
||||||
|
dmessage = dmessage.replace(u.getMention, "@" + u.getUsername) // TODO: IG Formatting
|
||||||
|
val m = u.asMember(DiscordPlugin.mainServer.getId).onErrorResume(_ => Mono.empty).blockOptional
|
||||||
|
if (m.isPresent) {
|
||||||
|
val mm: Member = m.get
|
||||||
|
val nick: String = mm.getDisplayName
|
||||||
|
dmessage = dmessage.replace(mm.getNicknameMention, "@" + nick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceUserMentions()
|
||||||
|
|
||||||
|
def replaceChannelMentions(): Unit = {
|
||||||
|
for (ch <- SFlux(event.getGuild.flux).flatMap(_.getChannels).toIterable()) {
|
||||||
|
dmessage = dmessage.replace(ch.getMention, "#" + ch.getName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceChannelMentions()
|
||||||
|
|
||||||
|
def replaceEmojis(): Unit = {
|
||||||
|
dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE) //Converts emoji to text- TODO: Add option to disable (resource pack?)
|
||||||
|
dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:") //Convert to Discord's format so it still shows up
|
||||||
|
dmessage = dmessage.replaceAll("<a?:(\\S+):(\\d+)>", ":$1:") //We don't need info about the custom emojis, just display their text
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceEmojis()
|
||||||
|
val clmd = MCChatCustom.getCustomChat(event.getMessage.getChannelId)
|
||||||
|
val sendChannel = event.getMessage.getChannel.block
|
||||||
|
val isPrivate = sendChannel.isInstanceOf[PrivateChannel]
|
||||||
|
|
||||||
|
def addCheckmark() = {
|
||||||
|
try {
|
||||||
|
val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage.getChannelId.asLong)
|
||||||
|
if (lmfd != null)
|
||||||
|
lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe // Remove it no matter what, we know it's there 99.99% of the time
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module)
|
||||||
|
}
|
||||||
|
MCChatUtils.lastmsgfromd.put(event.getMessage.getChannelId.asLong, event.getMessage)
|
||||||
|
event.getMessage.addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dmessage.startsWith("/")) // Ingame command
|
||||||
|
handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)
|
||||||
|
else if (handleIngameMessage(event, dmessage, dsender, user, clmd, isPrivate)) // Not a command
|
||||||
|
addCheckmark()
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a message coming from Discord to Minecraft.
|
||||||
|
*
|
||||||
|
* @param event The Discord event
|
||||||
|
* @param dmessage The message itself
|
||||||
|
* @param dsender The sender who sent it
|
||||||
|
* @param user The Chroma user of the sender
|
||||||
|
* @param clmd Custom chat last message data (if in a custom chat)
|
||||||
|
* @param isPrivate Whether the chat is private
|
||||||
|
* @return Whether the bot should react with a checkmark
|
||||||
|
*/
|
||||||
|
private def handleIngameMessage(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer,
|
||||||
|
clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Boolean = {
|
||||||
|
def getAttachmentText = {
|
||||||
|
val att = event.getMessage.getAttachments.asScala
|
||||||
|
if (att.nonEmpty) att map (_.getUrl) mkString "\n"
|
||||||
|
else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.getMessage.getType eq Message.Type.CHANNEL_PINNED_MESSAGE) {
|
||||||
|
val mcchannel = if (clmd != null) clmd.mcchannel else dsender.getChromaUser.channel.get
|
||||||
|
val rtr = mcchannel getRTR (if (clmd != null) clmd.dcp else dsender)
|
||||||
|
TBMCChatAPI.SendSystemMessage(mcchannel, rtr, (dsender match {
|
||||||
|
case player: Player => player.getDisplayName
|
||||||
|
case _ => dsender.getName
|
||||||
|
}) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val cmb = ChatMessage.builder(dsender, user, dmessage + getAttachmentText).fromCommand(false)
|
||||||
|
if (clmd != null)
|
||||||
|
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build, clmd.mcchannel)
|
||||||
|
else
|
||||||
|
TBMCChatAPI.SendChatMessage(cmb.build)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a Minecraft command coming from Discord.
|
||||||
|
*
|
||||||
|
* @param event The Discord event
|
||||||
|
* @param dmessage The Discord mewsage, starting with a slash
|
||||||
|
* @param dsender The sender who sent it
|
||||||
|
* @param user The Chroma user of the sender
|
||||||
|
* @param clmd The custom last message data (if in a custom chat)
|
||||||
|
* @param isPrivate Whether the chat is private
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private def handleIngameCommand(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer,
|
||||||
|
clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Unit = {
|
||||||
|
def notWhitelisted(cmd: String) = module.whitelistedCommands.get.stream
|
||||||
|
.noneMatch(s => cmd == s || cmd.startsWith(s + " "))
|
||||||
|
|
||||||
|
def whitelistedCommands = module.whitelistedCommands.get.stream
|
||||||
|
.map("/" + _).collect(Collectors.joining(", "))
|
||||||
|
|
||||||
|
if (!isPrivate)
|
||||||
|
event.getMessage.delete.subscribe()
|
||||||
|
val cmd = dmessage.substring(1)
|
||||||
|
val cmdlowercased = cmd.toLowerCase
|
||||||
|
if (dsender.isInstanceOf[DiscordSender] && notWhitelisted(cmdlowercased)) { // Command not whitelisted
|
||||||
|
dsender.sendMessage("Sorry, you can only access these commands from here:\n" + whitelistedCommands +
|
||||||
|
(if (user.getConnectedID(classOf[TBMCPlayer]) == null)
|
||||||
|
"\nTo access your commands, first please connect your accounts, using /connect in " + DPUtils.botmention
|
||||||
|
+ "\nThen y" else "\nY") + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!")
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
module.log(dsender.getName + " ran from DC: /" + cmd)
|
||||||
|
if (dsender.isInstanceOf[DiscordSender] && runCustomCommand(dsender, cmdlowercased)) {
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
val channel = if (clmd == null) user.channel.get else clmd.mcchannel
|
||||||
|
val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, if (clmd == null) dsender else clmd.dcp)
|
||||||
|
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => { //Commands need to be run sync
|
||||||
|
Bukkit.getPluginManager.callEvent(ev)
|
||||||
|
if (!ev.isCancelled)
|
||||||
|
runMCCommand(dsender, cmd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private def runMCCommand(dsender: DiscordSenderBase, cmd: String): Unit = {
|
||||||
|
try {
|
||||||
|
val mcpackage = Bukkit.getServer.getClass.getPackage.getName
|
||||||
|
if (!module.enableVanillaCommands.get)
|
||||||
|
Bukkit.dispatchCommand(dsender, cmd)
|
||||||
|
else if (mcpackage.contains("1_12"))
|
||||||
|
VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd)
|
||||||
|
else if (mcpackage.contains("1_14"))
|
||||||
|
VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd)
|
||||||
|
else if (mcpackage.contains("1_15") || mcpackage.contains("1_16"))
|
||||||
|
VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd)
|
||||||
|
else
|
||||||
|
Bukkit.dispatchCommand(dsender, cmd)
|
||||||
|
} catch {
|
||||||
|
case e: NoClassDefFoundError =>
|
||||||
|
TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module)
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles custom public commands. Used to hide sensitive information in public chats.
|
||||||
|
*
|
||||||
|
* @param dsender The Discord sender
|
||||||
|
* @param cmdlowercased The command, lowercased
|
||||||
|
* @return Whether the command was a custom command
|
||||||
|
*/
|
||||||
|
private def runCustomCommand(dsender: DiscordSenderBase, cmdlowercased: String): Boolean = {
|
||||||
|
if (cmdlowercased.startsWith("list")) {
|
||||||
|
val players = Bukkit.getOnlinePlayers
|
||||||
|
dsender.sendMessage("There are " + players.stream.filter(MCChatUtils.checkEssentials).count + " out of " + Bukkit.getMaxPlayers + " players online.")
|
||||||
|
dsender.sendMessage("Players: " + players.stream.filter(MCChatUtils.checkEssentials).map(_.getDisplayName).collect(Collectors.joining(", ")))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatUtils.LastMsgData
|
||||||
|
import buttondevteam.discordplugin.{DiscordConnectedPlayer, DiscordPlayer, DiscordPlugin, DiscordSenderBase}
|
||||||
|
import buttondevteam.lib.player.TBMCPlayer
|
||||||
|
import discord4j.core.`object`.entity.User
|
||||||
|
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scala.jdk.javaapi.CollectionConverters.asScala
|
||||||
|
|
||||||
|
object MCChatPrivate {
|
||||||
|
/**
|
||||||
|
* Used for messages in PMs (mcchat).
|
||||||
|
*/
|
||||||
|
private[mcchat] var lastmsgPerUser: ListBuffer[LastMsgData] = ListBuffer()
|
||||||
|
|
||||||
|
def privateMCChat(channel: MessageChannel, start: Boolean, user: User, dp: DiscordPlayer): Unit = {
|
||||||
|
MCChatUtils.ConnectedSenders synchronized {
|
||||||
|
val mcp = dp.getAs(classOf[TBMCPlayer])
|
||||||
|
if (mcp != null) { // If the accounts aren't connected, can't make a connected sender
|
||||||
|
val p = Bukkit.getPlayer(mcp.getUUID)
|
||||||
|
val op = Bukkit.getOfflinePlayer(mcp.getUUID)
|
||||||
|
val mcm = ComponentManager.getIfEnabled(classOf[MinecraftChatModule])
|
||||||
|
if (start) {
|
||||||
|
val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID, op.getName, mcm)
|
||||||
|
MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender)
|
||||||
|
MCChatUtils.LoggedInPlayers.put(mcp.getUUID, sender)
|
||||||
|
if (p == null) { // Player is offline - If the player is online, that takes precedence
|
||||||
|
MCChatUtils.callLoginEvents(sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId, user)
|
||||||
|
assert(sender != null)
|
||||||
|
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => {
|
||||||
|
def foo(): Unit = {
|
||||||
|
if ((p == null || p.isInstanceOf[DiscordSenderBase]) // Player is offline - If the player is online, that takes precedence
|
||||||
|
&& sender.isLoggedIn) { //Don't call the quit event if login failed
|
||||||
|
MCChatUtils.callLogoutEvent(sender, needsSync = false) //The next line has to run *after* this one, so can't use the needsSync parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId)
|
||||||
|
sender.setLoggedIn(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
foo()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// ---- PermissionsEx warning is normal on logout ----
|
||||||
|
}
|
||||||
|
if (!start) MCChatUtils.lastmsgfromd.remove(channel.getId.asLong)
|
||||||
|
if (start) lastmsgPerUser += new MCChatUtils.LastMsgData(channel, user) // Doesn't support group DMs
|
||||||
|
else lastmsgPerUser.filterInPlace(_.channel.getId.asLong != channel.getId.asLong) //Remove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isMinecraftChatEnabled(dp: DiscordPlayer): Boolean = isMinecraftChatEnabled(dp.getDiscordID)
|
||||||
|
|
||||||
|
def isMinecraftChatEnabled(did: String): Boolean = { // Don't load the player data just for this
|
||||||
|
lastmsgPerUser.exists(_.channel.asInstanceOf[PrivateChannel].getRecipientIds.stream.anyMatch(u => u.asString == did))
|
||||||
|
}
|
||||||
|
|
||||||
|
def logoutAll(): Unit = {
|
||||||
|
MCChatUtils.ConnectedSenders synchronized {
|
||||||
|
for ((_, userMap) <- MCChatUtils.ConnectedSenders) {
|
||||||
|
for (valueEntry <- asScala(userMap.entrySet)) {
|
||||||
|
if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey, valueEntry.getValue.getUser) == null) { //If the player is online then the fake player was already logged out
|
||||||
|
MCChatUtils.callLogoutEvent(valueEntry.getValue, !Bukkit.isPrimaryThread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,363 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.{ComponentManager, MainPlugin, component}
|
||||||
|
import buttondevteam.discordplugin.*
|
||||||
|
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
|
||||||
|
import buttondevteam.discordplugin.DPUtils.SpecExtensions
|
||||||
|
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatCustom.CustomLMD
|
||||||
|
import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent}
|
||||||
|
import com.google.common.collect.Sets
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.channel.{Channel, MessageChannel, PrivateChannel, TextChannel}
|
||||||
|
import discord4j.core.`object`.entity.{Message, User}
|
||||||
|
import discord4j.core.spec.legacy.LegacyTextChannelEditSpec
|
||||||
|
import io.netty.util.collection.LongObjectHashMap
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.Event
|
||||||
|
import org.bukkit.event.player.{AsyncPlayerPreLoginEvent, PlayerJoinEvent, PlayerLoginEvent, PlayerQuitEvent}
|
||||||
|
import org.bukkit.plugin.AuthorNagException
|
||||||
|
import reactor.core.publisher.{Flux as JFlux, Mono as JMono}
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import javax.annotation.Nullable
|
||||||
|
import scala.collection.concurrent
|
||||||
|
import scala.collection.convert.ImplicitConversions.`map AsJavaMap`
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava}
|
||||||
|
import scala.jdk.javaapi.CollectionConverters.asScala
|
||||||
|
|
||||||
|
object MCChatUtils {
|
||||||
|
/**
|
||||||
|
* May contain P<DiscordID> as key for public chat
|
||||||
|
*/
|
||||||
|
val UnconnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordSender]])
|
||||||
|
val ConnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordConnectedPlayer]])
|
||||||
|
val OnlineSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordPlayerSender]])
|
||||||
|
val LoggedInPlayers = asScala(new ConcurrentHashMap[UUID, DiscordConnectedPlayer])
|
||||||
|
@Nullable private[mcchat] var lastmsgdata: MCChatUtils.LastMsgData = null
|
||||||
|
private[mcchat] val lastmsgfromd = new LongObjectHashMap[Message] // Last message sent by a Discord user, used for clearing checkmarks
|
||||||
|
private var module: MinecraftChatModule = null
|
||||||
|
private val staticExcludedPlugins: concurrent.Map[Class[_ <: Event], util.HashSet[String]] = concurrent.TrieMap()
|
||||||
|
|
||||||
|
def updatePlayerList(): Unit = {
|
||||||
|
val mod = getModule
|
||||||
|
if (mod == null || !mod.showPlayerListOnDC.get) return ()
|
||||||
|
if (lastmsgdata != null) updatePL(lastmsgdata)
|
||||||
|
MCChatCustom.lastmsgCustom.foreach(MCChatUtils.updatePL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def notEnabled = (module == null || !module.disabling) && getModule == null //Allow using things while disabling the module
|
||||||
|
|
||||||
|
private def getModule = {
|
||||||
|
if (module == null || !module.isEnabled) module = ComponentManager.getIfEnabled(classOf[MinecraftChatModule])
|
||||||
|
//If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart
|
||||||
|
module
|
||||||
|
}
|
||||||
|
|
||||||
|
private def updatePL(lmd: MCChatUtils.LastMsgData): Unit = {
|
||||||
|
if (!lmd.channel.isInstanceOf[TextChannel]) {
|
||||||
|
TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId, new Exception("The channel isn't a (guild) text channel."), getModule)
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
var topic = lmd.channel.asInstanceOf[TextChannel].getTopic.orElse("")
|
||||||
|
if (topic.isEmpty) topic = ".\n----\nMinecraft chat\n----\n."
|
||||||
|
val s = topic.split("\\n----\\n")
|
||||||
|
if (s.length < 3) return ()
|
||||||
|
var gid: String = null
|
||||||
|
lmd match {
|
||||||
|
case clmd: CustomLMD => gid = clmd.groupID
|
||||||
|
case _ => //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most
|
||||||
|
gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE // (Though it's a public chat then rn)
|
||||||
|
}
|
||||||
|
val C = new AtomicInteger
|
||||||
|
s(s.length - 1) = "Players: " + Bukkit.getOnlinePlayers.stream.filter(p => if (lmd.mcchannel == null) {
|
||||||
|
gid == buttondevteam.core.component.channel.Channel.GROUP_EVERYONE //If null, allow if public (custom chats will have their channel stored anyway)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gid == lmd.mcchannel.getGroupID(p)
|
||||||
|
}
|
||||||
|
).filter(MCChatUtils.checkEssentials) //If they can see it
|
||||||
|
.filter(_ => C.incrementAndGet > 0) //Always true
|
||||||
|
.map((p) => DPUtils.sanitizeString(p.getDisplayName)).collect(Collectors.joining(", "))
|
||||||
|
s(0) = s"$C player${if (C.get != 1) "s" else ""} online"
|
||||||
|
lmd.channel.asInstanceOf[TextChannel].edit((tce: LegacyTextChannelEditSpec) =>
|
||||||
|
tce.setTopic(String.join("\n----\n", s: _*)).setReason("Player list update").^^()).subscribe //Don't wait
|
||||||
|
}
|
||||||
|
|
||||||
|
private[mcchat] def checkEssentials(p: Player): Boolean = {
|
||||||
|
val ess = MainPlugin.ess
|
||||||
|
if (ess == null) return true
|
||||||
|
!ess.getUser(p).isHidden
|
||||||
|
}
|
||||||
|
|
||||||
|
def addSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], user: User, sender: T): T =
|
||||||
|
addSender(senders, user.getId.asString, sender)
|
||||||
|
|
||||||
|
def addSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], did: String, sender: T): T = {
|
||||||
|
val mapOpt = senders.get(did)
|
||||||
|
val map = if (mapOpt.isEmpty) new ConcurrentHashMap[Snowflake, T] else mapOpt.get
|
||||||
|
map.put(sender.getChannel.getId, sender)
|
||||||
|
senders.put(did, map)
|
||||||
|
sender
|
||||||
|
}
|
||||||
|
|
||||||
|
def getSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = {
|
||||||
|
val mapOpt = senders.get(user.getId.asString)
|
||||||
|
if (mapOpt.nonEmpty) mapOpt.get.get(channel)
|
||||||
|
else null.asInstanceOf
|
||||||
|
}
|
||||||
|
|
||||||
|
def removeSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = {
|
||||||
|
val mapOpt = senders.get(user.getId.asString)
|
||||||
|
if (mapOpt.nonEmpty) mapOpt.get.remove(channel)
|
||||||
|
else null.asInstanceOf
|
||||||
|
}
|
||||||
|
|
||||||
|
def forPublicPrivateChat(action: SMono[MessageChannel] => SMono[_]): SMono[_] = {
|
||||||
|
if (notEnabled) return SMono.empty
|
||||||
|
val list = MCChatPrivate.lastmsgPerUser.map(data => action(SMono.just(data.channel)))
|
||||||
|
.prepend(action(module.chatChannelMono))
|
||||||
|
SMono(JMono.whenDelayError(list.asJava))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For custom and all MC chat
|
||||||
|
*
|
||||||
|
* @param action The action to act (cannot complete empty)
|
||||||
|
* @param toggle The toggle to check
|
||||||
|
* @param hookmsg Whether the message is also sent from the hook
|
||||||
|
*/
|
||||||
|
def forCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = {
|
||||||
|
if (notEnabled) return SMono.empty
|
||||||
|
val list = List(if (!GeneralEventBroadcasterModule.isHooked || !hookmsg)
|
||||||
|
forPublicPrivateChat(action) else SMono.empty) ++
|
||||||
|
(if (toggle == null) MCChatCustom.lastmsgCustom
|
||||||
|
else MCChatCustom.lastmsgCustom.filter(cc => (cc.toggles & (1 << toggle.id)) != 0))
|
||||||
|
.map(_.channel).map(SMono.just).map(action)
|
||||||
|
SMono(JMono.whenDelayError(list.asJava))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
|
||||||
|
*
|
||||||
|
* @param action The action to do
|
||||||
|
* @param sender The sender to check perms of or null to send to all that has it toggled
|
||||||
|
* @param toggle The toggle to check or null to send to all allowed
|
||||||
|
*/
|
||||||
|
def forAllowedCustomMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast): SMono[_] = {
|
||||||
|
if (notEnabled) return SMono.empty
|
||||||
|
val st = MCChatCustom.lastmsgCustom.filter(clmd => { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple
|
||||||
|
if (toggle != null && ((clmd.toggles & (1 << toggle.id)) == 0)) false //If null then allow
|
||||||
|
else if (sender == null) true
|
||||||
|
else clmd.groupID.equals(clmd.mcchannel.getGroupID(sender))
|
||||||
|
}).map(cc => action.apply(SMono.just(cc.channel))) //TODO: Send error messages on channel connect
|
||||||
|
//Mono.whenDelayError((() => st.iterator).asInstanceOf[java.lang.Iterable[Mono[_]]]) //Can't convert as an iterator or inside the stream, but I can convert it as a stream
|
||||||
|
SMono.whenDelayError(st)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
|
||||||
|
*
|
||||||
|
* @param action The action to do
|
||||||
|
* @param sender The sender to check perms of or null to send to all that has it toggled
|
||||||
|
* @param toggle The toggle to check or null to send to all allowed
|
||||||
|
* @param hookmsg Whether the message is also sent from the hook
|
||||||
|
*/
|
||||||
|
def forAllowedCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = {
|
||||||
|
if (notEnabled) return SMono.empty
|
||||||
|
val cc = forAllowedCustomMCChat(action, sender, toggle)
|
||||||
|
if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) return SMono.whenDelayError(Array(forPublicPrivateChat(action), cc))
|
||||||
|
SMono.whenDelayError(Array(cc))
|
||||||
|
}
|
||||||
|
|
||||||
|
def send(message: String): SMono[MessageChannel] => SMono[_] = _.flatMap((mc: MessageChannel) => {
|
||||||
|
resetLastMessage(mc)
|
||||||
|
SMono(mc.createMessage(DPUtils.sanitizeString(message)))
|
||||||
|
})
|
||||||
|
|
||||||
|
def forAllowedMCChat(action: SMono[MessageChannel] => SMono[_], event: TBMCSystemChatEvent): SMono[_] = {
|
||||||
|
if (notEnabled) return SMono.empty
|
||||||
|
val list = new ListBuffer[SMono[_]]
|
||||||
|
if (event.getChannel.isGlobal) list.append(action(module.chatChannelMono))
|
||||||
|
for (data <- MCChatPrivate.lastmsgPerUser)
|
||||||
|
if (event.shouldSendTo(getSender(data.channel.getId, data.user)))
|
||||||
|
list.append(action(SMono.just(data.channel))) //TODO: Only store ID?
|
||||||
|
MCChatCustom.lastmsgCustom.filter(clmd =>
|
||||||
|
clmd.brtoggles.contains(event.getTarget) && event.shouldSendTo(clmd.dcp))
|
||||||
|
.map(clmd => action(SMono.just(clmd.channel))).foreach(elem => {
|
||||||
|
list.append(elem)
|
||||||
|
()
|
||||||
|
})
|
||||||
|
SMono.whenDelayError(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc.
|
||||||
|
*/
|
||||||
|
private[mcchat] def getSender(channel: Snowflake, author: User): DiscordSenderBase = { //noinspection OptionalGetWithoutIsPresent
|
||||||
|
Option(getSender(OnlineSenders, channel, author)) // Find first non-null
|
||||||
|
.orElse(Option(getSender(ConnectedSenders, channel, author))) // This doesn't support the public chat, but it'll always return null for it
|
||||||
|
.orElse(Option(getSender(UnconnectedSenders, channel, author))) //
|
||||||
|
.orElse(Option(addSender(UnconnectedSenders, author,
|
||||||
|
new DiscordSender(author, SMono(DiscordPlugin.dc.getChannelById(channel)).block().asInstanceOf[MessageChannel]))))
|
||||||
|
.get
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the last message, so it will start a new one instead of appending to it.
|
||||||
|
* This is used when someone (even the bot) sends a message to the channel.
|
||||||
|
*
|
||||||
|
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
|
||||||
|
*/
|
||||||
|
def resetLastMessage(channel: Channel): Unit = {
|
||||||
|
if (notEnabled) return ()
|
||||||
|
if (channel.getId.asLong == module.chatChannel.get.asLong) {
|
||||||
|
if (lastmsgdata == null) lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null)
|
||||||
|
else lastmsgdata.message = null
|
||||||
|
return ()
|
||||||
|
} // Don't set the whole object to null, the player and channel information should be preserved
|
||||||
|
for (data <- if (channel.isInstanceOf[PrivateChannel]) MCChatPrivate.lastmsgPerUser
|
||||||
|
else MCChatCustom.lastmsgCustom) {
|
||||||
|
if (data.channel.getId.asLong == channel.getId.asLong) {
|
||||||
|
data.message = null
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//If it gets here, it's sending a message to a non-chat channel
|
||||||
|
}
|
||||||
|
|
||||||
|
def addStaticExcludedPlugin(event: Class[_ <: Event], plugin: String): util.HashSet[String] =
|
||||||
|
staticExcludedPlugins.compute(event, (_, hs: util.HashSet[String]) =>
|
||||||
|
if (hs == null) Sets.newHashSet(plugin) else if (hs.add(plugin)) hs else hs)
|
||||||
|
|
||||||
|
def callEventExcludingSome(event: Event): Unit = {
|
||||||
|
if (notEnabled) return ()
|
||||||
|
val second = staticExcludedPlugins.get(event.getClass)
|
||||||
|
val first = module.excludedPlugins.get
|
||||||
|
val both = if (second.isEmpty) first
|
||||||
|
else util.Arrays.copyOf(first, first.length + second.size)
|
||||||
|
var i = first.length
|
||||||
|
if (second.nonEmpty) {
|
||||||
|
for (plugin <- second.get.asScala) {
|
||||||
|
both(i) = plugin
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callEventExcluding(event, false, both: _*)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls an event with the given details.
|
||||||
|
* <p>
|
||||||
|
* This method only synchronizes when the event is not asynchronous.
|
||||||
|
*
|
||||||
|
* @param event Event details
|
||||||
|
* @param only Flips the operation and <b>includes</b> the listed plugins
|
||||||
|
* @param plugins The plugins to exclude. Not case sensitive.
|
||||||
|
*/
|
||||||
|
def callEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = { // Copied from Spigot-API and modified a bit
|
||||||
|
if (event.isAsynchronous) {
|
||||||
|
if (Thread.holdsLock(Bukkit.getPluginManager)) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from inside synchronized code.")
|
||||||
|
if (Bukkit.getServer.isPrimaryThread) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from primary server thread.")
|
||||||
|
fireEventExcluding(event, only, plugins: _*)
|
||||||
|
}
|
||||||
|
else Bukkit.getPluginManager synchronized fireEventExcluding(event, only, plugins: _*)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def fireEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = {
|
||||||
|
val handlers = event.getHandlers // Code taken from SimplePluginManager in Spigot-API
|
||||||
|
val listeners = handlers.getRegisteredListeners
|
||||||
|
val server = Bukkit.getServer
|
||||||
|
for (registration <- listeners) { // Modified to exclude plugins
|
||||||
|
if (registration.getPlugin.isEnabled
|
||||||
|
&& !plugins.exists(only ^ _.equalsIgnoreCase(registration.getPlugin.getName))) try registration.callEvent(event)
|
||||||
|
catch {
|
||||||
|
case ex: AuthorNagException =>
|
||||||
|
val plugin = registration.getPlugin
|
||||||
|
if (plugin.isNaggable) {
|
||||||
|
plugin.setNaggable(false)
|
||||||
|
server.getLogger.log(Level.SEVERE, String.format("Nag author(s): '%s' of '%s' about the following: %s", plugin.getDescription.getAuthors, plugin.getDescription.getFullName, ex.getMessage))
|
||||||
|
}
|
||||||
|
case ex: Throwable =>
|
||||||
|
server.getLogger.log(Level.SEVERE, "Could not pass event " + event.getEventName + " to " + registration.getPlugin.getDescription.getFullName, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call it from an async thread.
|
||||||
|
*/
|
||||||
|
def callLoginEvents(dcp: DiscordConnectedPlayer): Unit = {
|
||||||
|
val loginFail = (kickMsg: String) => {
|
||||||
|
dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg)
|
||||||
|
MCChatPrivate.privateMCChat(dcp.getChannel, start = false, dcp.getUser, dcp.getChromaUser)
|
||||||
|
} //Probably also happens if the user is banned or so
|
||||||
|
val event = new AsyncPlayerPreLoginEvent(dcp.getName, InetAddress.getLoopbackAddress, dcp.getUniqueId)
|
||||||
|
callEventExcludingSome(event)
|
||||||
|
if (event.getLoginResult ne AsyncPlayerPreLoginEvent.Result.ALLOWED) {
|
||||||
|
loginFail(event.getKickMessage)
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => {
|
||||||
|
def foo(): Unit = {
|
||||||
|
val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress)
|
||||||
|
callEventExcludingSome(ev)
|
||||||
|
if (ev.getResult ne PlayerLoginEvent.Result.ALLOWED) {
|
||||||
|
loginFail(ev.getKickMessage)
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
callEventExcludingSome(new PlayerJoinEvent(dcp, ""))
|
||||||
|
dcp.setLoggedIn(true)
|
||||||
|
if (module != null) {
|
||||||
|
if (module.serverWatcher != null) module.serverWatcher.fakePlayers.add(dcp)
|
||||||
|
module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged in from Discord")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only calls the events if the player is actually logged in
|
||||||
|
*
|
||||||
|
* @param dcp The player
|
||||||
|
* @param needsSync Whether we're in an async thread
|
||||||
|
*/
|
||||||
|
def callLogoutEvent(dcp: DiscordConnectedPlayer, needsSync: Boolean): Unit = {
|
||||||
|
if (!dcp.isLoggedIn) return ()
|
||||||
|
val event = new PlayerQuitEvent(dcp, "")
|
||||||
|
if (needsSync) callEventSync(event)
|
||||||
|
else callEventExcludingSome(event)
|
||||||
|
dcp.setLoggedIn(false)
|
||||||
|
if (module != null) {
|
||||||
|
module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged out from Discord")
|
||||||
|
if (module.serverWatcher != null) module.serverWatcher.fakePlayers.remove(dcp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[mcchat] def callEventSync(event: Event) = Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => callEventExcludingSome(event))
|
||||||
|
|
||||||
|
class LastMsgData(val channel: MessageChannel, val user: User) {
|
||||||
|
var message: Message = null
|
||||||
|
var time = 0L
|
||||||
|
var content: String = null
|
||||||
|
var mcchannel: component.channel.Channel = null
|
||||||
|
|
||||||
|
protected def this(channel: MessageChannel, user: User, mcchannel: component.channel.Channel) = {
|
||||||
|
this(channel, user)
|
||||||
|
this.mcchannel = mcchannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.*
|
||||||
|
import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions}
|
||||||
|
import buttondevteam.lib.TBMCSystemChatEvent
|
||||||
|
import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase, TBMCYEEHAWEvent}
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.Role
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import net.ess3.api.events.{AfkStatusChangeEvent, MuteStatusChangeEvent, NickChangeEvent, VanishStatusChangeEvent}
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.entity.PlayerDeathEvent
|
||||||
|
import org.bukkit.event.player.*
|
||||||
|
import org.bukkit.event.player.PlayerLoginEvent.Result
|
||||||
|
import org.bukkit.event.server.{BroadcastMessageEvent, TabCompleteEvent}
|
||||||
|
import org.bukkit.event.{EventHandler, EventPriority, Listener}
|
||||||
|
import reactor.core.scala.publisher.{SFlux, SMono}
|
||||||
|
|
||||||
|
class MCListener(val module: MinecraftChatModule) extends Listener {
|
||||||
|
final private val muteRole = DPUtils.roleData(module.getConfig, "muteRole", "Muted")
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST) def onPlayerLogin(e: PlayerLoginEvent): Unit = {
|
||||||
|
if (e.getResult ne Result.ALLOWED) return ()
|
||||||
|
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return ()
|
||||||
|
val dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId)
|
||||||
|
if (dcp.nonEmpty) MCChatUtils.callLogoutEvent(dcp.get, needsSync = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR) def onPlayerJoin(e: PlayerJoinEvent): Unit = {
|
||||||
|
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Don't show the joined message for the fake player
|
||||||
|
Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => {
|
||||||
|
def foo(): Unit = {
|
||||||
|
val p = e.getPlayer
|
||||||
|
val dp = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordPlayer])
|
||||||
|
if (dp != null)
|
||||||
|
DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID)).^^().flatMap(user =>
|
||||||
|
user.getPrivateChannel.^^().flatMap(chan => module.chatChannelMono.flatMap(cc => {
|
||||||
|
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, chan, p, module))
|
||||||
|
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, cc, p, module)) //Stored per-channel
|
||||||
|
SMono.empty
|
||||||
|
}))).subscribe()
|
||||||
|
val message = e.getJoinMessage
|
||||||
|
sendJoinLeaveMessage(message, e.getPlayer)
|
||||||
|
ChromaBot.updatePlayerList()
|
||||||
|
}
|
||||||
|
|
||||||
|
foo()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private def sendJoinLeaveMessage(message: String, player: Player): Unit =
|
||||||
|
if (message != null && message.trim.nonEmpty)
|
||||||
|
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), player, ChannelconBroadcast.JOINLEAVE, hookmsg = true).subscribe()
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR) def onPlayerLeave(e: PlayerQuitEvent): Unit = {
|
||||||
|
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Only care about real users
|
||||||
|
MCChatUtils.OnlineSenders.filterInPlace((_, userMap) => userMap.entrySet.stream.noneMatch(_.getValue.getUniqueId.equals(e.getPlayer.getUniqueId)))
|
||||||
|
Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId).foreach(MCChatUtils.callLoginEvents))
|
||||||
|
Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => ChromaBot.updatePlayerList(), 5)
|
||||||
|
val message = e.getQuitMessage
|
||||||
|
sendJoinLeaveMessage(message, e.getPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST) def onPlayerKick(e: PlayerKickEvent): Unit = {
|
||||||
|
/*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting")
|
||||||
|
&& !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special
|
||||||
|
MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOW) def onPlayerDeath(e: PlayerDeathEvent): Unit =
|
||||||
|
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage), e.getEntity, ChannelconBroadcast.DEATH, hookmsg = true).subscribe()
|
||||||
|
|
||||||
|
@EventHandler def onPlayerAFK(e: AfkStatusChangeEvent): Unit = {
|
||||||
|
val base = e.getAffected.getBase
|
||||||
|
if (e.isCancelled || !base.isOnline) return ()
|
||||||
|
val msg = base.getDisplayName + " is " + (if (e.getValue) "now"
|
||||||
|
else "no longer") + " AFK."
|
||||||
|
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, hookmsg = false).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onPlayerMute(e: MuteStatusChangeEvent): Unit = {
|
||||||
|
val role = muteRole.get
|
||||||
|
if (role == null) return ()
|
||||||
|
val source = e.getAffected.getSource
|
||||||
|
if (!source.isPlayer) return ()
|
||||||
|
val p = TBMCPlayerBase.getPlayer(source.getPlayer.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordPlayer])
|
||||||
|
if (p == null) return ()
|
||||||
|
DPUtils.ignoreError(SMono(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID)))
|
||||||
|
.flatMap(user => SMono(user.asMember(DiscordPlugin.mainServer.getId)))
|
||||||
|
.flatMap(user => role.flatMap((r: Role) => {
|
||||||
|
def foo(r: Role): SMono[_] = {
|
||||||
|
if (e.getValue) user.addRole(r.getId)
|
||||||
|
else user.removeRole(r.getId)
|
||||||
|
val modlog = module.modlogChannel.get
|
||||||
|
val msg = (if (e.getValue) "M"
|
||||||
|
else "Unm") + "uted user: " + user.getUsername + "#" + user.getDiscriminator
|
||||||
|
module.log(msg)
|
||||||
|
if (modlog != null) return modlog.flatMap((ch: MessageChannel) => SMono(ch.createMessage(msg)))
|
||||||
|
SMono.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(r)
|
||||||
|
}))).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onChatSystemMessage(event: TBMCSystemChatEvent): Unit =
|
||||||
|
MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage), event).subscribe()
|
||||||
|
|
||||||
|
@EventHandler def onBroadcastMessage(event: BroadcastMessageEvent): Unit =
|
||||||
|
MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage), ChannelconBroadcast.BROADCAST, hookmsg = false).subscribe()
|
||||||
|
|
||||||
|
@EventHandler def onYEEHAW(event: TBMCYEEHAWEvent): Unit = { //TODO: Inherit from the chat event base to have channel support
|
||||||
|
val name = event.getSender match {
|
||||||
|
case player: Player => player.getDisplayName
|
||||||
|
case _ => event.getSender.getName
|
||||||
|
}
|
||||||
|
//Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO
|
||||||
|
DiscordPlugin.mainServer.getEmojis.^^().filter(e => "YEEHAW" == e.getName).take(1).singleOrEmpty
|
||||||
|
.map(Option.apply).defaultIfEmpty(Option.empty)
|
||||||
|
.flatMap(yeehaw => MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name +
|
||||||
|
yeehaw.map(guildEmoji => " <:YEEHAW:" + guildEmoji.getId.asString + ">s").getOrElse(" YEEHAWs")))).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onNickChange(event: NickChangeEvent): Unit = MCChatUtils.updatePlayerList()
|
||||||
|
|
||||||
|
@EventHandler def onTabComplete(event: TabCompleteEvent): Unit = {
|
||||||
|
val i = event.getBuffer.lastIndexOf(' ')
|
||||||
|
val t = event.getBuffer.substring(i + 1) //0 if not found
|
||||||
|
if (!t.startsWith("@")) return ()
|
||||||
|
val token = t.substring(1)
|
||||||
|
val x = DiscordPlugin.mainServer.getMembers.^^().flatMap(m => SFlux.just(m.getUsername, m.getNickname.orElse("")))
|
||||||
|
.filter(_.startsWith(token)).map("@" + _).doOnNext(event.getCompletions.add(_)).blockLast()
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler def onCommandSend(event: PlayerCommandSendEvent): Boolean = event.getCommands.add("g")
|
||||||
|
|
||||||
|
@EventHandler def onVanish(event: VanishStatusChangeEvent): Unit = {
|
||||||
|
if (event.isCancelled) return ()
|
||||||
|
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => MCChatUtils.updatePlayerList())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
package buttondevteam.discordplugin.mcchat
|
||||||
|
|
||||||
|
import buttondevteam.core.component.channel.Channel
|
||||||
|
import buttondevteam.discordplugin.DPUtils.{MonoExtensions, SpecExtensions}
|
||||||
|
import buttondevteam.discordplugin.playerfaker.ServerWatcher
|
||||||
|
import buttondevteam.discordplugin.playerfaker.perm.LPInjector
|
||||||
|
import buttondevteam.discordplugin.util.DPState
|
||||||
|
import buttondevteam.discordplugin.{ChannelconBroadcast, DPUtils, DiscordConnectedPlayer, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.architecture.{Component, ConfigData, ReadOnlyConfigData}
|
||||||
|
import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent}
|
||||||
|
import com.google.common.collect.Lists
|
||||||
|
import discord4j.common.util.Snowflake
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import discord4j.rest.util.Color
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import java.util.{Objects, UUID}
|
||||||
|
import scala.jdk.CollectionConverters.IterableHasAsScala
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM.
|
||||||
|
*/
|
||||||
|
object MinecraftChatModule {
|
||||||
|
var state = DPState.RUNNING
|
||||||
|
}
|
||||||
|
|
||||||
|
class MinecraftChatModule extends Component[DiscordPlugin] {
|
||||||
|
def getListener: MCChatListener = this.listener
|
||||||
|
|
||||||
|
private var listener: MCChatListener = null
|
||||||
|
private[mcchat] var serverWatcher: ServerWatcher = null
|
||||||
|
private var lpInjector: LPInjector = null
|
||||||
|
private[mcchat] var disabling = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command!
|
||||||
|
*/
|
||||||
|
val whitelistedCommands: ConfigData[util.ArrayList[String]] = getConfig.getData("whitelistedCommands",
|
||||||
|
() => Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki", "yeehaw", "lenny", "rp", "plugins"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel to use as the public Minecraft chat - everything public gets broadcasted here
|
||||||
|
*/
|
||||||
|
val chatChannel: ReadOnlyConfigData[Snowflake] = DPUtils.snowflakeData(getConfig, "chatChannel", 0L)
|
||||||
|
|
||||||
|
def chatChannelMono: SMono[MessageChannel] = DPUtils.getMessageChannel(chatChannel.getPath, chatChannel.get)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute
|
||||||
|
*/
|
||||||
|
val modlogChannel: ReadOnlyConfigData[SMono[MessageChannel]] = DPUtils.channelData(getConfig, "modlogChannel")
|
||||||
|
/**
|
||||||
|
* The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here
|
||||||
|
*/
|
||||||
|
val excludedPlugins: ConfigData[Array[String]] = getConfig.getData("excludedPlugins", Array[String]("ProtocolLib", "LibsDisguises", "JourneyMapServer"))
|
||||||
|
/**
|
||||||
|
* If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands.
|
||||||
|
* They can then use commands like /tpahere to teleport others to that place.<br />
|
||||||
|
* If this is off, then teleporting will have no effect.
|
||||||
|
*/
|
||||||
|
val allowFakePlayerTeleports: ConfigData[Boolean] = getConfig.getData("allowFakePlayerTeleports", false)
|
||||||
|
/**
|
||||||
|
* If this is on, each chat channel will have a player list in their description.
|
||||||
|
* It only gets added if there's no description yet or there are (at least) two lines of "----" following each other.
|
||||||
|
* Note that it will replace <b>everything</b> above the first and below the last "----" but it will only detect exactly four dashes.
|
||||||
|
* So if you want to use dashes for something else in the description, make sure it's either less or more dashes in one line.
|
||||||
|
*/
|
||||||
|
val showPlayerListOnDC: ConfigData[Boolean] = getConfig.getData("showPlayerListOnDC", true)
|
||||||
|
/**
|
||||||
|
* This setting controls whether custom chat connections can be <i>created</i> (existing connections will always work).
|
||||||
|
* Custom chat connections can be created using the channelcon command and they allow players to display town chat in a Discord channel for example.
|
||||||
|
* See the channelcon command for more details.
|
||||||
|
*/
|
||||||
|
val allowCustomChat: ConfigData[Boolean] = getConfig.getData("allowCustomChat", true)
|
||||||
|
/**
|
||||||
|
* This setting allows you to control if players can DM the bot to log on the server from Discord.
|
||||||
|
* This allows them to both chat and perform any command they can in-game.
|
||||||
|
*/
|
||||||
|
val allowPrivateChat: ConfigData[Boolean] = getConfig.getData("allowPrivateChat", true)
|
||||||
|
/**
|
||||||
|
* If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID.
|
||||||
|
*/
|
||||||
|
val profileURL: ConfigData[String] = getConfig.getData("profileURL", "")
|
||||||
|
/**
|
||||||
|
* Enables support for running vanilla commands through Discord, if you ever need it.
|
||||||
|
*/
|
||||||
|
val enableVanillaCommands: ConfigData[Boolean] = getConfig.getData("enableVanillaCommands", true)
|
||||||
|
/**
|
||||||
|
* Whether players logged on from Discord (mcchat command) should be recognised by other plugins. Some plugins might break if it's turned off.
|
||||||
|
* But it's really hacky.
|
||||||
|
*/
|
||||||
|
final private val addFakePlayersToBukkit = getConfig.getData("addFakePlayersToBukkit", false)
|
||||||
|
/**
|
||||||
|
* Set by the component to report crashes.
|
||||||
|
*/
|
||||||
|
final private val serverUp = getConfig.getData("serverUp", false)
|
||||||
|
final private val mcChatCommand = new MCChatCommand(this)
|
||||||
|
final private val channelconCommand = new ChannelconCommand(this)
|
||||||
|
|
||||||
|
override protected def enable(): Unit = {
|
||||||
|
if (DPUtils.disableIfConfigErrorRes(this, chatChannel, chatChannelMono)) return ()
|
||||||
|
listener = new MCChatListener(this)
|
||||||
|
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin)
|
||||||
|
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin) //These get undone if restarting/resetting - it will ignore events if disabled
|
||||||
|
getPlugin.manager.registerCommand(mcChatCommand)
|
||||||
|
getPlugin.manager.registerCommand(channelconCommand)
|
||||||
|
val chcons = getConfig.getConfig.getConfigurationSection("chcons")
|
||||||
|
if (chcons == null) { //Fallback to old place
|
||||||
|
getConfig.getConfig.getRoot.getConfigurationSection("chcons")
|
||||||
|
}
|
||||||
|
if (chcons != null) {
|
||||||
|
val chconkeys = chcons.getKeys(false)
|
||||||
|
for (chconkey <- chconkeys.asScala) {
|
||||||
|
val chcon = chcons.getConfigurationSection(chconkey)
|
||||||
|
val mcch = Channel.getChannels.filter((ch: Channel) => ch.ID == chcon.getString("mcchid")).findAny
|
||||||
|
val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block
|
||||||
|
val did = chcon.getLong("did")
|
||||||
|
val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block
|
||||||
|
val groupid = chcon.getString("groupid")
|
||||||
|
val toggles = chcon.getInt("toggles")
|
||||||
|
val brtoggles = chcon.getStringList("brtoggles")
|
||||||
|
if (mcch.isPresent && ch != null && user != null && groupid != null) {
|
||||||
|
Bukkit.getScheduler.runTask(getPlugin, () => { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase)
|
||||||
|
val dcp = DiscordConnectedPlayer.create(user, ch.asInstanceOf[MessageChannel],
|
||||||
|
UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this)
|
||||||
|
MCChatCustom.addCustomChat(ch.asInstanceOf[MessageChannel], groupid, mcch.get, user, dcp, toggles,
|
||||||
|
brtoggles.asScala.map(TBMCSystemChatEvent.BroadcastTarget.get).filter(Objects.nonNull).toSet)
|
||||||
|
()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try if (lpInjector == null) lpInjector = new LPInjector //new LPInjector(DiscordPlugin.plugin)
|
||||||
|
catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e, this)
|
||||||
|
case e: NoClassDefFoundError =>
|
||||||
|
log("No LuckPerms, not injecting")
|
||||||
|
//e.printStackTrace();
|
||||||
|
}
|
||||||
|
if (addFakePlayersToBukkit.get) try {
|
||||||
|
serverWatcher = new ServerWatcher
|
||||||
|
serverWatcher.enableDisable(true)
|
||||||
|
log("Finished hooking into the server")
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Failed to hack the server (object)! Disable addFakePlayersToBukkit in the config.", e, this)
|
||||||
|
}
|
||||||
|
if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) { //These will only execute if the chat is enabled
|
||||||
|
sendStateMessage(Color.CYAN, "Discord plugin restarted - chat connected.") //Really important to note the chat, hmm
|
||||||
|
MinecraftChatModule.state = DPState.RUNNING
|
||||||
|
}
|
||||||
|
else if (MinecraftChatModule.state eq DPState.DISABLED_MCCHAT) {
|
||||||
|
sendStateMessage(Color.CYAN, "Minecraft chat enabled - chat connected.")
|
||||||
|
MinecraftChatModule.state = DPState.RUNNING
|
||||||
|
}
|
||||||
|
else if (serverUp.get) {
|
||||||
|
sendStateMessage(Color.YELLOW, "Server started after a crash - chat connected.")
|
||||||
|
val thr = new Throwable("The server shut down unexpectedly. See the log of the previous run for more details.")
|
||||||
|
thr.setStackTrace(new Array[StackTraceElement](0))
|
||||||
|
TBMCCoreAPI.SendException("The server crashed!", thr, this)
|
||||||
|
}
|
||||||
|
else sendStateMessage(Color.GREEN, "Server started - chat connected.")
|
||||||
|
serverUp.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def disable(): Unit = {
|
||||||
|
disabling = true
|
||||||
|
if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) sendStateMessage(Color.ORANGE, "Discord plugin restarting")
|
||||||
|
else if (MinecraftChatModule.state eq DPState.RUNNING) {
|
||||||
|
sendStateMessage(Color.ORANGE, "Minecraft chat disabled")
|
||||||
|
MinecraftChatModule.state = DPState.DISABLED_MCCHAT
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val kickmsg = if (Bukkit.getOnlinePlayers.size > 0)
|
||||||
|
DPUtils.sanitizeString(Bukkit.getOnlinePlayers.stream.map(_.getDisplayName).collect(Collectors.joining(", "))) +
|
||||||
|
(if (Bukkit.getOnlinePlayers.size == 1) " was " else " were ") + "thrown out" //TODO: Make configurable
|
||||||
|
else ""
|
||||||
|
if (MinecraftChatModule.state eq DPState.RESTARTING_SERVER) sendStateMessage(Color.ORANGE, "Server restarting", kickmsg)
|
||||||
|
else if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) sendStateMessage(Color.RED, "Server stopping", kickmsg)
|
||||||
|
else sendStateMessage(Color.GRAY, "Unknown state, please report.")
|
||||||
|
//If 'restart' is disabled then this isn't shown even if joinleave is enabled}
|
||||||
|
}
|
||||||
|
serverUp.set(false) //Disable even if just the component is disabled because that way it won't falsely report crashes
|
||||||
|
try //If it's not enabled it won't do anything
|
||||||
|
if (serverWatcher != null) {
|
||||||
|
serverWatcher.enableDisable(false)
|
||||||
|
log("Finished unhooking the server")
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Failed to restore the server object!", e, this)
|
||||||
|
}
|
||||||
|
val chcons = MCChatCustom.getCustomChats
|
||||||
|
val chconsc = getConfig.getConfig.createSection("chcons")
|
||||||
|
for (chcon <- chcons) {
|
||||||
|
val chconc = chconsc.createSection(chcon.channel.getId.asString)
|
||||||
|
chconc.set("mcchid", chcon.mcchannel.ID)
|
||||||
|
chconc.set("chid", chcon.channel.getId.asLong)
|
||||||
|
chconc.set("did", chcon.user.getId.asLong)
|
||||||
|
chconc.set("mcuid", chcon.dcp.getUniqueId.toString)
|
||||||
|
chconc.set("mcname", chcon.dcp.getName)
|
||||||
|
chconc.set("groupid", chcon.groupID)
|
||||||
|
chconc.set("toggles", chcon.toggles)
|
||||||
|
chconc.set("brtoggles", chcon.brtoggles.map(_.getName).toList)
|
||||||
|
}
|
||||||
|
if (listener != null) { //Can be null if disabled because of a config error
|
||||||
|
listener.stop(true)
|
||||||
|
}
|
||||||
|
getPlugin.manager.unregisterCommand(mcChatCommand)
|
||||||
|
getPlugin.manager.unregisterCommand(channelconCommand)
|
||||||
|
disabling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It will block to make sure all messages are sent
|
||||||
|
*/
|
||||||
|
private def sendStateMessage(color: Color, message: String) =
|
||||||
|
MCChatUtils.forCustomAndAllMCChat(_.flatMap(
|
||||||
|
_.createEmbed(_.setColor(color).setTitle(message).^^()).^^()
|
||||||
|
.onErrorResume(_ => SMono.empty)
|
||||||
|
), ChannelconBroadcast.RESTART, hookmsg = false).block()
|
||||||
|
|
||||||
|
private def sendStateMessage(color: Color, message: String, extra: String) =
|
||||||
|
MCChatUtils.forCustomAndAllMCChat(_.flatMap(
|
||||||
|
_.createEmbed(_.setColor(color).setTitle(message).setDescription(extra).^^()).^^()
|
||||||
|
.onErrorResume(_ => SMono.empty)
|
||||||
|
), ChannelconBroadcast.RESTART, hookmsg = false).block()
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
package buttondevteam.discordplugin.mccommands
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.commands.{ConnectCommand, VersionCommand}
|
||||||
|
import buttondevteam.discordplugin.mcchat.{MCChatUtils, MinecraftChatModule}
|
||||||
|
import buttondevteam.discordplugin.util.DPState
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin, DiscordSenderBase}
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass, ICommand2MC}
|
||||||
|
import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer, TBMCPlayerBase}
|
||||||
|
import discord4j.core.`object`.ExtendedInvite
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
@CommandClass(path = "discord", helpText = Array(
|
||||||
|
"Discord",
|
||||||
|
"This command allows performing Discord-related actions."
|
||||||
|
)) class DiscordMCCommand extends ICommand2MC {
|
||||||
|
@Command2.Subcommand def accept(player: Player): Boolean = {
|
||||||
|
if (checkSafeMode(player)) return true
|
||||||
|
val did = ConnectCommand.WaitingToConnect.get(player.getName)
|
||||||
|
if (did == null) {
|
||||||
|
player.sendMessage("§cYou don't have a pending connection to Discord.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val dp = ChromaGamerBase.getUser(did, classOf[DiscordPlayer])
|
||||||
|
val mcp = TBMCPlayerBase.getPlayer(player.getUniqueId, classOf[TBMCPlayer])
|
||||||
|
dp.connectWith(mcp)
|
||||||
|
ConnectCommand.WaitingToConnect.remove(player.getName)
|
||||||
|
MCChatUtils.UnconnectedSenders.remove(did) //Remove all unconnected, will be recreated where needed
|
||||||
|
player.sendMessage("§bAccounts connected.")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand def decline(player: Player): Boolean = {
|
||||||
|
if (checkSafeMode(player)) return true
|
||||||
|
val did = ConnectCommand.WaitingToConnect.remove(player.getName)
|
||||||
|
if (did == null) {
|
||||||
|
player.sendMessage("§cYou don't have a pending connection to Discord.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
player.sendMessage("§bPending connection declined.")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = Array(
|
||||||
|
"Reload Discord plugin",
|
||||||
|
"Reloads the config. To apply some changes, you may need to also run /discord restart."
|
||||||
|
)) def reload(sender: CommandSender): Unit =
|
||||||
|
if (DiscordPlugin.plugin.tryReloadConfig) sender.sendMessage("§bConfig reloaded.")
|
||||||
|
else sender.sendMessage("§cFailed to reload config.")
|
||||||
|
|
||||||
|
@Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = Array(
|
||||||
|
"Restart the plugin", //
|
||||||
|
"This command disables and then enables the plugin." //
|
||||||
|
)) def restart(sender: CommandSender): Unit = {
|
||||||
|
val task: Runnable = () => {
|
||||||
|
def foo(): Unit = {
|
||||||
|
if (!DiscordPlugin.plugin.tryReloadConfig) {
|
||||||
|
sender.sendMessage("§cFailed to reload config so not restarting. Check the console.")
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
MinecraftChatModule.state = DPState.RESTARTING_PLUGIN //Reset in MinecraftChatModule
|
||||||
|
sender.sendMessage("§bDisabling DiscordPlugin...")
|
||||||
|
Bukkit.getPluginManager.disablePlugin(DiscordPlugin.plugin)
|
||||||
|
if (!sender.isInstanceOf[DiscordSenderBase]) { //Sending to Discord errors
|
||||||
|
sender.sendMessage("§bEnabling DiscordPlugin...")
|
||||||
|
}
|
||||||
|
Bukkit.getPluginManager.enablePlugin(DiscordPlugin.plugin)
|
||||||
|
if (!sender.isInstanceOf[DiscordSenderBase]) sender.sendMessage("§bRestart finished!")
|
||||||
|
}
|
||||||
|
|
||||||
|
foo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Bukkit.getName == "Paper")) {
|
||||||
|
getPlugin.getLogger.warning("Async plugin events are not supported by the server, running on main thread")
|
||||||
|
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, task)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand(helpText = Array(
|
||||||
|
"Version command",
|
||||||
|
"Prints the plugin version")) def version(sender: CommandSender): Unit = {
|
||||||
|
sender.sendMessage(VersionCommand.getVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand(helpText = Array(
|
||||||
|
"Invite",
|
||||||
|
"Shows an invite link to the server"
|
||||||
|
)) def invite(sender: CommandSender): Unit = {
|
||||||
|
if (checkSafeMode(sender)) {
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
val invi: String = DiscordPlugin.plugin.inviteLink.get
|
||||||
|
if (invi.nonEmpty) {
|
||||||
|
sender.sendMessage("§bInvite link: " + invi)
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
DiscordPlugin.mainServer.getInvites.limitRequest(1)
|
||||||
|
.switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("§cNo invites found for the server.")))
|
||||||
|
.subscribe((inv: ExtendedInvite) => sender.sendMessage("§bInvite link: https://discord.gg/" + inv.getCode), _ => sender.sendMessage("§cThe invite link is not set and the bot has no permission to get it."))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] = {
|
||||||
|
method.getName match {
|
||||||
|
case "accept" =>
|
||||||
|
Array[String]("Accept Discord connection", "Accept a pending connection between your Discord and Minecraft account.", "To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention + " channel on Discord")
|
||||||
|
case "decline" =>
|
||||||
|
Array[String]("Decline Discord connection", "Decline a pending connection between your Discord and Minecraft account.", "To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention + " channel on Discord")
|
||||||
|
case _ =>
|
||||||
|
super.getHelpText(method, ann)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def checkSafeMode(sender: CommandSender): Boolean = {
|
||||||
|
if (DiscordPlugin.SafeMode) {
|
||||||
|
sender.sendMessage("§cThe plugin isn't initialized. Check console for details.")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import org.mockito.MockedConstruction
|
||||||
|
import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker
|
||||||
|
import org.mockito.invocation.MockHandler
|
||||||
|
import org.mockito.mock.MockCreationSettings
|
||||||
|
import org.mockito.plugins.MockMaker
|
||||||
|
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
object DelegatingMockMaker {
|
||||||
|
def getInstance: DelegatingMockMaker = DelegatingMockMaker.instance
|
||||||
|
|
||||||
|
private var instance: DelegatingMockMaker = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class DelegatingMockMaker() extends MockMaker {
|
||||||
|
DelegatingMockMaker.instance = this
|
||||||
|
|
||||||
|
override def createMock[T](settings: MockCreationSettings[T], handler: MockHandler[_]): T =
|
||||||
|
this.mockMaker.createMock(settings, handler)
|
||||||
|
|
||||||
|
override def createSpy[T](settings: MockCreationSettings[T], handler: MockHandler[_], instance: T): Optional[T] =
|
||||||
|
this.mockMaker.createSpy(settings, handler, instance)
|
||||||
|
|
||||||
|
override def getHandler(mock: Any): MockHandler[_] =
|
||||||
|
this.mockMaker.getHandler(mock)
|
||||||
|
|
||||||
|
override def resetMock(mock: Any, newHandler: MockHandler[_], settings: MockCreationSettings[_]): Unit = {
|
||||||
|
this.mockMaker.resetMock(mock, newHandler, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isTypeMockable(`type`: Class[_]): MockMaker.TypeMockability =
|
||||||
|
this.mockMaker.isTypeMockable(`type`)
|
||||||
|
|
||||||
|
override def createStaticMock[T](`type`: Class[T], settings: MockCreationSettings[T], handler: MockHandler[_]): MockMaker.StaticMockControl[T] =
|
||||||
|
this.mockMaker.createStaticMock(`type`, settings, handler)
|
||||||
|
|
||||||
|
override def createConstructionMock[T](`type`: Class[T], settingsFactory: java.util.function.Function[MockedConstruction.Context,
|
||||||
|
MockCreationSettings[T]], handlerFactory: java.util.function.Function[MockedConstruction.Context,
|
||||||
|
MockHandler[T]], mockInitializer: MockedConstruction.MockInitializer[T]): MockMaker.ConstructionMockControl[T] =
|
||||||
|
this.mockMaker.createConstructionMock[T](`type`, settingsFactory, handlerFactory, mockInitializer)
|
||||||
|
|
||||||
|
def setMockMaker(mockMaker: MockMaker): Unit = {
|
||||||
|
this.mockMaker = mockMaker
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMockMaker: MockMaker = this.mockMaker
|
||||||
|
|
||||||
|
private var mockMaker: MockMaker = new SubclassByteBuddyMockMaker
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
package buttondevteam.discordplugin.playerfaker;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.HumanEntity;
|
import org.bukkit.entity.HumanEntity;
|
||||||
|
@ -16,8 +14,7 @@ import java.util.stream.IntStream;
|
||||||
public class DiscordInventory implements Inventory {
|
public class DiscordInventory implements Inventory {
|
||||||
private ItemStack[] items = new ItemStack[27];
|
private ItemStack[] items = new ItemStack[27];
|
||||||
private List<ItemStack> itemStacks = Arrays.asList(items);
|
private List<ItemStack> itemStacks = Arrays.asList(items);
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public int maxStackSize;
|
public int maxStackSize;
|
||||||
private static ItemStack emptyStack = new ItemStack(Material.AIR, 0);
|
private static ItemStack emptyStack = new ItemStack(Material.AIR, 0);
|
||||||
|
|
||||||
|
@ -26,6 +23,16 @@ public class DiscordInventory implements Inventory {
|
||||||
return items.length;
|
return items.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxStackSize() {
|
||||||
|
return maxStackSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMaxStackSize(int maxStackSize) {
|
||||||
|
this.maxStackSize = maxStackSize;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Discord inventory";
|
return "Discord inventory";
|
|
@ -0,0 +1,94 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordConnectedPlayer
|
||||||
|
import buttondevteam.discordplugin.mcchat.MCChatUtils
|
||||||
|
import com.destroystokyo.paper.profile.CraftPlayerProfile
|
||||||
|
import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.{Bukkit, Server}
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
|
||||||
|
import org.mockito.invocation.InvocationOnMock
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.util
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object ServerWatcher {
|
||||||
|
|
||||||
|
class AppendListView[T](private val originalList: java.util.List[T], private val additionalList: java.util.List[T]) extends java.util.AbstractSequentialList[T] {
|
||||||
|
|
||||||
|
override def listIterator(i: Int): util.ListIterator[T] = {
|
||||||
|
val os = originalList.size
|
||||||
|
if (i < os) originalList.listIterator(i)
|
||||||
|
else additionalList.listIterator(i - os)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def size: Int = originalList.size + additionalList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerWatcher {
|
||||||
|
final val fakePlayers = new util.ArrayList[Player]
|
||||||
|
private var origServer: Server = null
|
||||||
|
|
||||||
|
@IgnoreForBinding
|
||||||
|
@throws[Exception]
|
||||||
|
def enableDisable(enable: Boolean): Unit = {
|
||||||
|
val serverField = classOf[Bukkit].getDeclaredField("server")
|
||||||
|
serverField.setAccessible(true)
|
||||||
|
if (enable) {
|
||||||
|
val serverClass = Bukkit.getServer.getClass
|
||||||
|
val originalServer = serverField.get(null)
|
||||||
|
DelegatingMockMaker.getInstance.setMockMaker(new InlineByteBuddyMockMaker)
|
||||||
|
val settings = Mockito.withSettings.stubOnly.defaultAnswer((invocation: InvocationOnMock) => {
|
||||||
|
def foo(invocation: InvocationOnMock): AnyRef = {
|
||||||
|
val method = invocation.getMethod
|
||||||
|
val pc = method.getParameterCount
|
||||||
|
var player = Option.empty[DiscordConnectedPlayer]
|
||||||
|
method.getName match {
|
||||||
|
case "getPlayer" =>
|
||||||
|
if (pc == 1 && (method.getParameterTypes()(0) == classOf[UUID]))
|
||||||
|
player = MCChatUtils.LoggedInPlayers.get(invocation.getArgument[UUID](0))
|
||||||
|
case "getPlayerExact" =>
|
||||||
|
if (pc == 1) {
|
||||||
|
val argument = invocation.getArgument(0)
|
||||||
|
player = MCChatUtils.LoggedInPlayers.values.find(_.getName.equalsIgnoreCase(argument))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*case "getOnlinePlayers":
|
||||||
|
if (playerList == null) {
|
||||||
|
@SuppressWarnings("unchecked") var list = (List<Player>) method.invoke(origServer, invocation.getArguments());
|
||||||
|
playerList = new AppendListView<>(list, fakePlayers);
|
||||||
|
} - Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.
|
||||||
|
return playerList;*/
|
||||||
|
case "createProfile" => //Paper's method, casts the player to a CraftPlayer
|
||||||
|
if (pc == 2) {
|
||||||
|
val uuid = invocation.getArgument(0)
|
||||||
|
val name = invocation.getArgument(1)
|
||||||
|
player = if (uuid != null) MCChatUtils.LoggedInPlayers.get(uuid) else Option.empty
|
||||||
|
if (player.isEmpty && name != null)
|
||||||
|
player = MCChatUtils.LoggedInPlayers.values.find(_.getName.equalsIgnoreCase(name))
|
||||||
|
if (player.nonEmpty)
|
||||||
|
return new CraftPlayerProfile(player.get.getUniqueId, player.get.getName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (player.nonEmpty) return player.get
|
||||||
|
method.invoke(origServer, invocation.getArguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
foo(invocation)
|
||||||
|
})
|
||||||
|
//var mock = mockMaker.createMock(settings, MockHandlerFactory.createMockHandler(settings));
|
||||||
|
//thread.setContextClassLoader(cl);
|
||||||
|
val mock = Mockito.mock(serverClass, settings)
|
||||||
|
for (field <- serverClass.getFields) { //Copy public fields, private fields aren't accessible directly anyways
|
||||||
|
if (!Modifier.isFinal(field.getModifiers) && !Modifier.isStatic(field.getModifiers)) field.set(mock, field.get(originalServer))
|
||||||
|
}
|
||||||
|
serverField.set(null, mock)
|
||||||
|
origServer = originalServer.asInstanceOf[Server]
|
||||||
|
}
|
||||||
|
else if (origServer != null) serverField.set(null, origServer)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
|
||||||
|
import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
object VCMDWrapper {
|
||||||
|
/**
|
||||||
|
* This constructor will only send raw vanilla messages to the sender in plain text.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
*/
|
||||||
|
def createListener[T <: DiscordSenderBase with IMCPlayer[T]](player: T, module: MinecraftChatModule): AnyRef =
|
||||||
|
createListener(player, null, module)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
* @param bukkitplayer The Bukkit player to send the raw message to
|
||||||
|
* @param module The Minecraft chat module
|
||||||
|
*/
|
||||||
|
def createListener[T <: DiscordSenderBase with IMCPlayer[T]](player: T, bukkitplayer: Player, module: MinecraftChatModule): AnyRef = try {
|
||||||
|
var ret: AnyRef = null
|
||||||
|
val mcpackage = Bukkit.getServer.getClass.getPackage.getName
|
||||||
|
if (mcpackage.contains("1_12")) ret = new VanillaCommandListener[T](player, bukkitplayer)
|
||||||
|
else if (mcpackage.contains("1_14")) ret = new VanillaCommandListener14[T](player, bukkitplayer)
|
||||||
|
else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) ret = VanillaCommandListener15.create(player, bukkitplayer) //bukkitplayer may be null but that's fine
|
||||||
|
else ret = null
|
||||||
|
if (ret == null) compatWarning(module)
|
||||||
|
ret
|
||||||
|
} catch {
|
||||||
|
case e@(_: NoClassDefFoundError | _: Exception) =>
|
||||||
|
compatWarning(module)
|
||||||
|
TBMCCoreAPI.SendException("Failed to create vanilla command listener", e, module)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private def compatWarning(module: MinecraftChatModule): Unit =
|
||||||
|
module.logWarn("Vanilla commands won't be available from Discord due to a compatibility error. Disable vanilla command support to remove this message.")
|
||||||
|
|
||||||
|
private[playerfaker] def compatResponse(dsender: DiscordSenderBase) = {
|
||||||
|
dsender.sendMessage("Vanilla commands are not supported on this Minecraft version.")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VCMDWrapper(private val listener: AnyRef) {
|
||||||
|
@javax.annotation.Nullable def getListener: AnyRef = listener
|
||||||
|
|
||||||
|
//Needed to mock the player @Nullable
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer}
|
||||||
|
import net.minecraft.server.v1_12_R1.*
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer
|
||||||
|
import org.bukkit.craftbukkit.v1_12_R1.{CraftServer, CraftWorld}
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
|
||||||
|
object VanillaCommandListener {
|
||||||
|
def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = {
|
||||||
|
val cmd = Bukkit.getServer.asInstanceOf[CraftServer].getCommandMap.getCommand(cmdstr.split(" ")(0).toLowerCase)
|
||||||
|
if (!dsender.isInstanceOf[Player] || !cmd.isInstanceOf[VanillaCommandWrapper])
|
||||||
|
return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds
|
||||||
|
if (!dsender.isInstanceOf[IMCPlayer[_]])
|
||||||
|
throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.")
|
||||||
|
val sender = dsender.asInstanceOf[IMCPlayer[_]]
|
||||||
|
val vcmd = cmd.asInstanceOf[VanillaCommandWrapper]
|
||||||
|
if (!vcmd.testPermission(sender)) return true
|
||||||
|
val icommandlistener = sender.getVanillaCmdListener.getListener.asInstanceOf[ICommandListener]
|
||||||
|
if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender)
|
||||||
|
var args = cmdstr.split(" ")
|
||||||
|
args = util.Arrays.copyOfRange(args, 1, args.length)
|
||||||
|
try vcmd.dispatchVanillaCommand(sender, icommandlistener, args)
|
||||||
|
catch {
|
||||||
|
case commandexception: CommandException =>
|
||||||
|
// Taken from CommandHandler
|
||||||
|
val chatmessage = new ChatMessage(commandexception.getMessage, commandexception.getArgs)
|
||||||
|
chatmessage.getChatModifier.setColor(EnumChatFormat.RED)
|
||||||
|
icommandlistener.sendMessage(chatmessage)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VanillaCommandListener[T <: DiscordSenderBase with IMCPlayer[T]] extends ICommandListener {
|
||||||
|
def getPlayer: T = this.player
|
||||||
|
|
||||||
|
private var player: T = null.asInstanceOf
|
||||||
|
private var bukkitplayer: Player = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will only send raw vanilla messages to the sender in plain text.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
*/
|
||||||
|
def this(player: T) = {
|
||||||
|
this()
|
||||||
|
this.player = player
|
||||||
|
this.bukkitplayer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
* @param bukkitplayer The Bukkit player to send the raw message to
|
||||||
|
*/
|
||||||
|
def this(player: T, bukkitplayer: Player) = {
|
||||||
|
this()
|
||||||
|
this.player = player
|
||||||
|
this.bukkitplayer = bukkitplayer
|
||||||
|
if (bukkitplayer != null && !bukkitplayer.isInstanceOf[CraftPlayer])
|
||||||
|
throw new ClassCastException("bukkitplayer must be a Bukkit player!")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def C_(): MinecraftServer = Bukkit.getServer.asInstanceOf[CraftServer].getServer
|
||||||
|
|
||||||
|
override def a(oplevel: Int, cmd: String): Boolean = { //return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands
|
||||||
|
oplevel == 0 || player.isOp
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getName: String = player.getName
|
||||||
|
|
||||||
|
override def getWorld: World = player.getWorld.asInstanceOf[CraftWorld].getHandle
|
||||||
|
|
||||||
|
override def sendMessage(arg0: IChatBaseComponent): Unit = {
|
||||||
|
player.sendMessage(arg0.toPlainText)
|
||||||
|
if (bukkitplayer != null) bukkitplayer.asInstanceOf[CraftPlayer].getHandle.sendMessage(arg0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer}
|
||||||
|
import net.minecraft.server.v1_14_R1.*
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.craftbukkit.v1_14_R1.command.{ProxiedNativeCommandSender, VanillaCommandWrapper}
|
||||||
|
import org.bukkit.craftbukkit.v1_14_R1.entity.CraftPlayer
|
||||||
|
import org.bukkit.craftbukkit.v1_14_R1.{CraftServer, CraftWorld}
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
|
import java.util
|
||||||
|
|
||||||
|
object VanillaCommandListener14 {
|
||||||
|
def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = {
|
||||||
|
val cmd = Bukkit.getServer.asInstanceOf[CraftServer].getCommandMap.getCommand(cmdstr.split(" ")(0).toLowerCase)
|
||||||
|
if (!dsender.isInstanceOf[Player] || !cmd.isInstanceOf[VanillaCommandWrapper])
|
||||||
|
return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds
|
||||||
|
if (!dsender.isInstanceOf[IMCPlayer[_]])
|
||||||
|
throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.")
|
||||||
|
val sender = dsender.asInstanceOf[IMCPlayer[_]] // Don't use val on recursive interfaces :P
|
||||||
|
val vcmd = cmd.asInstanceOf[VanillaCommandWrapper]
|
||||||
|
if (!vcmd.testPermission(sender)) return true
|
||||||
|
val world = Bukkit.getWorlds.get(0).asInstanceOf[CraftWorld].getHandle
|
||||||
|
val icommandlistener = sender.getVanillaCmdListener.getListener.asInstanceOf[ICommandListener]
|
||||||
|
if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender)
|
||||||
|
val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), new Vec2F(0, 0), world, 0, sender.getName, new ChatComponentText(sender.getName), world.getMinecraftServer, null)
|
||||||
|
val pncs = new ProxiedNativeCommandSender(wrapper, sender, sender)
|
||||||
|
var args = cmdstr.split(" ")
|
||||||
|
args = util.Arrays.copyOfRange(args, 1, args.length)
|
||||||
|
try return vcmd.execute(pncs, cmd.getLabel, args)
|
||||||
|
catch {
|
||||||
|
case commandexception: CommandException =>
|
||||||
|
// Taken from CommandHandler
|
||||||
|
val chatmessage = new ChatMessage(commandexception.getMessage, commandexception.a)
|
||||||
|
chatmessage.getChatModifier.setColor(EnumChatFormat.RED)
|
||||||
|
icommandlistener.sendMessage(chatmessage)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VanillaCommandListener14[T <: DiscordSenderBase with IMCPlayer[T]] extends ICommandListener {
|
||||||
|
def getPlayer: T = this.player
|
||||||
|
|
||||||
|
private var player: T = null.asInstanceOf
|
||||||
|
private var bukkitplayer: Player = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will only send raw vanilla messages to the sender in plain text.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
*/
|
||||||
|
def this(player: T) = {
|
||||||
|
this()
|
||||||
|
this.player = player
|
||||||
|
this.bukkitplayer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
* @param bukkitplayer The Bukkit player to send the raw message to
|
||||||
|
*/
|
||||||
|
def this(player: T, bukkitplayer: Player) = {
|
||||||
|
this()
|
||||||
|
this.player = player
|
||||||
|
this.bukkitplayer = bukkitplayer
|
||||||
|
if (bukkitplayer != null && !bukkitplayer.isInstanceOf[CraftPlayer]) throw new ClassCastException("bukkitplayer must be a Bukkit player!")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def sendMessage(arg0: IChatBaseComponent): scala.Unit = {
|
||||||
|
player.sendMessage(arg0.getString)
|
||||||
|
if (bukkitplayer != null) bukkitplayer.asInstanceOf[CraftPlayer].getHandle.sendMessage(arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def shouldSendSuccess = true
|
||||||
|
|
||||||
|
override def shouldSendFailure = true
|
||||||
|
|
||||||
|
override def shouldBroadcastCommands = true //Broadcast to in-game admins
|
||||||
|
override def getBukkitSender(commandListenerWrapper: CommandListenerWrapper): CommandSender = player
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package buttondevteam.discordplugin.playerfaker
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer}
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.command.{CommandSender, SimpleCommandMap}
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.mockito.{Answers, Mockito}
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link VanillaCommandListener14} but with reflection
|
||||||
|
*/
|
||||||
|
object VanillaCommandListener15 {
|
||||||
|
private var vcwcl: Class[_] = null
|
||||||
|
private var nms: String = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will only send raw vanilla messages to the sender in plain text.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
*/
|
||||||
|
@throws[Exception]
|
||||||
|
def create[T <: DiscordSenderBase with IMCPlayer[T]](player: T): VanillaCommandListener15[T] = create(player, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
|
||||||
|
*
|
||||||
|
* @param player The Discord sender player (the wrapper)
|
||||||
|
* @param bukkitplayer The Bukkit player to send the raw message to
|
||||||
|
*/
|
||||||
|
@SuppressWarnings(Array("unchecked"))
|
||||||
|
@throws[Exception]
|
||||||
|
def create[T <: DiscordSenderBase with IMCPlayer[T]](player: T, bukkitplayer: Player): VanillaCommandListener15[T] = {
|
||||||
|
if (vcwcl == null) {
|
||||||
|
val pkg = Bukkit.getServer.getClass.getPackage.getName
|
||||||
|
vcwcl = Class.forName(pkg + ".command.VanillaCommandWrapper")
|
||||||
|
}
|
||||||
|
if (nms == null) {
|
||||||
|
val server = Bukkit.getServer
|
||||||
|
nms = server.getClass.getMethod("getServer").invoke(server).getClass.getPackage.getName //org.mockito.codegen
|
||||||
|
}
|
||||||
|
val iclcl = Class.forName(nms + ".ICommandListener")
|
||||||
|
Mockito.mock(classOf[VanillaCommandListener15[T]],
|
||||||
|
Mockito.withSettings.stubOnly.useConstructor(player, bukkitplayer)
|
||||||
|
.extraInterfaces(iclcl).defaultAnswer(invocation => {
|
||||||
|
if (invocation.getMethod.getName == "sendMessage") {
|
||||||
|
val icbc = invocation.getArgument(0)
|
||||||
|
player.sendMessage(icbc.getClass.getMethod("getString").invoke(icbc).asInstanceOf[String])
|
||||||
|
if (bukkitplayer != null) {
|
||||||
|
val handle = bukkitplayer.getClass.getMethod("getHandle").invoke(bukkitplayer)
|
||||||
|
handle.getClass.getMethod("sendMessage", icbc.getClass).invoke(handle, icbc)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else if (!Modifier.isAbstract(invocation.getMethod.getModifiers)) invocation.callRealMethod
|
||||||
|
else if (invocation.getMethod.getReturnType eq classOf[Boolean]) true //shouldSend... shouldBroadcast...
|
||||||
|
else if (invocation.getMethod.getReturnType eq classOf[CommandSender]) player
|
||||||
|
else Answers.RETURNS_DEFAULTS.answer(invocation)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
@throws[Exception]
|
||||||
|
def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = {
|
||||||
|
val server = Bukkit.getServer
|
||||||
|
val cmap = server.getClass.getMethod("getCommandMap").invoke(server).asInstanceOf[SimpleCommandMap]
|
||||||
|
val cmd = cmap.getCommand(cmdstr.split(" ")(0).toLowerCase)
|
||||||
|
if (!dsender.isInstanceOf[Player] || cmd == null || !vcwcl.isAssignableFrom(cmd.getClass))
|
||||||
|
return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds
|
||||||
|
if (!dsender.isInstanceOf[IMCPlayer[_]])
|
||||||
|
throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.")
|
||||||
|
val sender = dsender.asInstanceOf[IMCPlayer[_]] // Don't use val on recursive interfaces :P
|
||||||
|
if (!vcwcl.getMethod("testPermission", classOf[CommandSender]).invoke(cmd, sender).asInstanceOf[Boolean])
|
||||||
|
return true
|
||||||
|
val cworld = Bukkit.getWorlds.get(0)
|
||||||
|
val world = cworld.getClass.getMethod("getHandle").invoke(cworld)
|
||||||
|
val icommandlistener = sender.getVanillaCmdListener.getListener
|
||||||
|
if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender)
|
||||||
|
val clwcl = Class.forName(nms + ".CommandListenerWrapper")
|
||||||
|
val v3dcl = Class.forName(nms + ".Vec3D")
|
||||||
|
val v2fcl = Class.forName(nms + ".Vec2F")
|
||||||
|
val icbcl = Class.forName(nms + ".IChatBaseComponent")
|
||||||
|
val mcscl = Class.forName(nms + ".MinecraftServer")
|
||||||
|
val ecl = Class.forName(nms + ".Entity")
|
||||||
|
val cctcl = Class.forName(nms + ".ChatComponentText")
|
||||||
|
val iclcl = Class.forName(nms + ".ICommandListener")
|
||||||
|
val wrapper = clwcl.getConstructor(iclcl, v3dcl, v2fcl, world.getClass, classOf[Int], classOf[String], icbcl, mcscl, ecl)
|
||||||
|
.newInstance(icommandlistener, v3dcl.getConstructor(classOf[Double], classOf[Double], classOf[Double])
|
||||||
|
.newInstance(0, 0, 0), v2fcl.getConstructor(classOf[Float], classOf[Float])
|
||||||
|
.newInstance(0, 0), world, 0, sender.getName, cctcl.getConstructor(classOf[String])
|
||||||
|
.newInstance(sender.getName), world.getClass.getMethod("getMinecraftServer").invoke(world), null)
|
||||||
|
/*val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0),
|
||||||
|
new Vec2F(0, 0), world, 0, sender.getName(),
|
||||||
|
new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);*/
|
||||||
|
val pncscl = Class.forName(vcwcl.getPackage.getName + ".ProxiedNativeCommandSender")
|
||||||
|
val pncs = pncscl.getConstructor(clwcl, classOf[CommandSender], classOf[CommandSender])
|
||||||
|
.newInstance(wrapper, sender, sender)
|
||||||
|
var args = cmdstr.split(" ")
|
||||||
|
args = util.Arrays.copyOfRange(args, 1, args.length)
|
||||||
|
try return cmd.execute(pncs.asInstanceOf[CommandSender], cmd.getLabel, args)
|
||||||
|
catch {
|
||||||
|
case commandexception: Exception =>
|
||||||
|
if (!(commandexception.getClass.getSimpleName == "CommandException")) throw commandexception
|
||||||
|
// Taken from CommandHandler
|
||||||
|
val cmcl = Class.forName(nms + ".ChatMessage")
|
||||||
|
val chatmessage = cmcl.getConstructor(classOf[String], classOf[Array[AnyRef]])
|
||||||
|
.newInstance(commandexception.getMessage, Array[AnyRef](commandexception.getClass.getMethod("a").invoke(commandexception)))
|
||||||
|
val modifier = cmcl.getMethod("getChatModifier").invoke(chatmessage)
|
||||||
|
val ecfcl = Class.forName(nms + ".EnumChatFormat")
|
||||||
|
modifier.getClass.getMethod("setColor", ecfcl).invoke(modifier, ecfcl.getField("RED").get(null))
|
||||||
|
icommandlistener.getClass.getMethod("sendMessage", icbcl).invoke(icommandlistener, chatmessage)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VanillaCommandListener15[T <: DiscordSenderBase with IMCPlayer[T]] protected(var player: T, val bukkitplayer: Player) {
|
||||||
|
if (bukkitplayer != null && !bukkitplayer.getClass.getSimpleName.endsWith("CraftPlayer"))
|
||||||
|
throw new ClassCastException("bukkitplayer must be a Bukkit player!")
|
||||||
|
|
||||||
|
def getPlayer: T = this.player
|
||||||
|
}
|
|
@ -1,40 +1,9 @@
|
||||||
package buttondevteam.discordplugin.playerfaker.perm;
|
package buttondevteam.discordplugin.playerfaker.perm;
|
||||||
|
|
||||||
import buttondevteam.discordplugin.DiscordConnectedPlayer;
|
|
||||||
import buttondevteam.discordplugin.DiscordPlugin;
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
|
||||||
import buttondevteam.lib.TBMCCoreAPI;
|
|
||||||
import me.lucko.luckperms.bukkit.LPBukkitBootstrap;
|
|
||||||
import me.lucko.luckperms.bukkit.LPBukkitPlugin;
|
|
||||||
import me.lucko.luckperms.bukkit.inject.permissible.DummyPermissibleBase;
|
|
||||||
import me.lucko.luckperms.bukkit.inject.permissible.LuckPermsPermissible;
|
|
||||||
import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener;
|
|
||||||
import me.lucko.luckperms.common.config.ConfigKeys;
|
|
||||||
import me.lucko.luckperms.common.locale.Message;
|
|
||||||
import me.lucko.luckperms.common.locale.TranslationManager;
|
|
||||||
import me.lucko.luckperms.common.model.User;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.EventPriority;
|
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.PlayerLoginEvent;
|
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
|
||||||
import org.bukkit.permissions.PermissibleBase;
|
|
||||||
import org.bukkit.permissions.PermissionAttachment;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public final class LPInjector implements Listener { //Disable login event for LuckPerms
|
public final class LPInjector implements Listener { //Disable login event for LuckPerms
|
||||||
private final LPBukkitPlugin plugin;
|
/*private final LPBukkitPlugin plugin;
|
||||||
private final BukkitConnectionListener connectionListener;
|
private final BukkitConnectionListener connectionListener;
|
||||||
private final Set<UUID> deniedLogin;
|
private final Set<UUID> deniedLogin;
|
||||||
private final Field detectedCraftBukkitOfflineMode;
|
private final Field detectedCraftBukkitOfflineMode;
|
||||||
|
@ -86,11 +55,11 @@ public final class LPInjector implements Listener { //Disable login event for Lu
|
||||||
|
|
||||||
//Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener
|
//Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener
|
||||||
@EventHandler(priority = EventPriority.LOWEST)
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
public void onPlayerLogin(PlayerLoginEvent e) {
|
public void onPlayerLogin(PlayerLoginEvent e) {*/
|
||||||
/* Called when the player starts logging into the server.
|
/* Called when the player starts logging into the server.
|
||||||
At this point, the users data should be present and loaded. */
|
At this point, the users data should be present and loaded. */
|
||||||
|
|
||||||
if (!(e.getPlayer() instanceof DiscordConnectedPlayer))
|
/*if (!(e.getPlayer() instanceof DiscordConnectedPlayer))
|
||||||
return; //Normal players must be handled by the plugin
|
return; //Normal players must be handled by the plugin
|
||||||
|
|
||||||
final DiscordConnectedPlayer player = (DiscordConnectedPlayer) e.getPlayer();
|
final DiscordConnectedPlayer player = (DiscordConnectedPlayer) e.getPlayer();
|
||||||
|
@ -102,7 +71,7 @@ public final class LPInjector implements Listener { //Disable login event for Lu
|
||||||
final User user = plugin.getUserManager().getIfLoaded(player.getUniqueId());
|
final User user = plugin.getUserManager().getIfLoaded(player.getUniqueId());
|
||||||
|
|
||||||
/* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */
|
/* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */
|
||||||
if (user == null) {
|
/*if (user == null) {
|
||||||
deniedLogin.add(player.getUniqueId());
|
deniedLogin.add(player.getUniqueId());
|
||||||
|
|
||||||
if (!plugin.getConnectionListener().getUniqueConnections().contains(player.getUniqueId())) {
|
if (!plugin.getConnectionListener().getUniqueConnections().contains(player.getUniqueId())) {
|
||||||
|
@ -234,5 +203,5 @@ public final class LPInjector implements Listener { //Disable login event for Lu
|
||||||
player.setPerm(DummyPermissibleBase.INSTANCE);
|
player.setPerm(DummyPermissibleBase.INSTANCE);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package buttondevteam.discordplugin.role
|
||||||
|
|
||||||
|
import buttondevteam.core.ComponentManager
|
||||||
|
import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions}
|
||||||
|
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
|
||||||
|
import buttondevteam.lib.architecture.{Component, ComponentMetadata}
|
||||||
|
import discord4j.core.`object`.entity.Role
|
||||||
|
import discord4j.core.`object`.entity.channel.MessageChannel
|
||||||
|
import discord4j.core.event.domain.role.{RoleCreateEvent, RoleDeleteEvent, RoleEvent, RoleUpdateEvent}
|
||||||
|
import discord4j.rest.util.Color
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import reactor.core.scala.publisher.SMono
|
||||||
|
|
||||||
|
import java.util.Collections
|
||||||
|
import scala.jdk.CollectionConverters.SeqHasAsJava
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically collects roles with a certain color.
|
||||||
|
* Users can add these roles to themselves using the /role Discord command.
|
||||||
|
*/
|
||||||
|
@ComponentMetadata(enabledByDefault = false) object GameRoleModule {
|
||||||
|
def handleRoleEvent(roleEvent: RoleEvent): Unit = {
|
||||||
|
val grm = ComponentManager.getIfEnabled(classOf[GameRoleModule])
|
||||||
|
if (grm == null) return ()
|
||||||
|
val GameRoles = grm.GameRoles
|
||||||
|
val logChannel = grm.logChannel.get
|
||||||
|
val notMainServer = (_: Role).getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong
|
||||||
|
roleEvent match {
|
||||||
|
case roleCreateEvent: RoleCreateEvent => Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => {
|
||||||
|
val role = roleCreateEvent.getRole
|
||||||
|
if (!notMainServer(role)) {
|
||||||
|
grm.isGameRole(role).flatMap(b => {
|
||||||
|
if (!b) SMono.empty //Deleted or not a game role
|
||||||
|
else {
|
||||||
|
GameRoles.add(role.getName)
|
||||||
|
if (logChannel != null)
|
||||||
|
logChannel.flatMap(_.createMessage("Added " + role.getName + " as game role." +
|
||||||
|
" If you don't want this, change the role's color from the game role color.").^^())
|
||||||
|
else
|
||||||
|
SMono.empty
|
||||||
|
}
|
||||||
|
}).subscribe()
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
case roleDeleteEvent: RoleDeleteEvent =>
|
||||||
|
val role = roleDeleteEvent.getRole.orElse(null)
|
||||||
|
if (role == null) return ()
|
||||||
|
if (notMainServer(role)) return ()
|
||||||
|
if (GameRoles.remove(role.getName) && logChannel != null)
|
||||||
|
logChannel.flatMap(_.createMessage("Removed " + role.getName + " as a game role.").^^()).subscribe()
|
||||||
|
case roleUpdateEvent: RoleUpdateEvent =>
|
||||||
|
if (!roleUpdateEvent.getOld.isPresent) {
|
||||||
|
grm.logWarn("Old role not stored, cannot update game role!")
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
val or = roleUpdateEvent.getOld.get
|
||||||
|
if (notMainServer(or)) return ()
|
||||||
|
val cr = roleUpdateEvent.getCurrent
|
||||||
|
grm.isGameRole(cr).flatMap(isGameRole => {
|
||||||
|
if (!isGameRole)
|
||||||
|
if (GameRoles.remove(or.getName) && logChannel != null)
|
||||||
|
logChannel.flatMap(_.createMessage("Removed " + or.getName + " as a game role because its color changed.").^^())
|
||||||
|
else
|
||||||
|
SMono.empty
|
||||||
|
else if (GameRoles.contains(or.getName) && or.getName == cr.getName)
|
||||||
|
SMono.empty
|
||||||
|
else {
|
||||||
|
val removed = GameRoles.remove(or.getName) //Regardless of whether it was a game role
|
||||||
|
GameRoles.add(cr.getName) //Add it because it has no color
|
||||||
|
if (logChannel != null)
|
||||||
|
if (removed)
|
||||||
|
logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Changed game role from " + or.getName + " to " + cr.getName + ".").^^())
|
||||||
|
else
|
||||||
|
logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Added " + cr.getName + " as game role because it has the color of one.").^^())
|
||||||
|
else
|
||||||
|
SMono.empty
|
||||||
|
}
|
||||||
|
}).subscribe()
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentMetadata(enabledByDefault = false) class GameRoleModule extends Component[DiscordPlugin] {
|
||||||
|
var GameRoles: java.util.List[String] = null
|
||||||
|
final private val command = new RoleCommand(this)
|
||||||
|
|
||||||
|
override protected def enable(): Unit = {
|
||||||
|
getPlugin.manager.registerCommand(command)
|
||||||
|
GameRoles = DiscordPlugin.mainServer.getRoles.^^().filterWhen(this.isGameRole).map(_.getName).collectSeq().block().asJava
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def disable(): Unit = getPlugin.manager.unregisterCommand(command)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel where the bot logs when it detects a role change that results in a new game role or one being removed.
|
||||||
|
*/
|
||||||
|
final private val logChannel = DPUtils.channelData(getConfig, "logChannel")
|
||||||
|
/**
|
||||||
|
* The role color that is used by game roles.
|
||||||
|
* Defaults to the second to last in the upper row - #95a5a6.
|
||||||
|
*/
|
||||||
|
final private val roleColor = getConfig.getConfig[Color]("roleColor").`def`(Color.of(149, 165, 166)).getter((rgb: Any) => Color.of(Integer.parseInt(rgb.asInstanceOf[String].substring(1), 16))).setter((color: Color) => String.format("#%08x", color.getRGB)).buildReadOnly
|
||||||
|
|
||||||
|
private def isGameRole(r: Role): SMono[Boolean] = {
|
||||||
|
if (r.getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong) return SMono.just(false) //Only allow on the main server
|
||||||
|
val rc = roleColor.get
|
||||||
|
if (r.getColor equals rc)
|
||||||
|
DiscordPlugin.dc.getSelf.flatMap(u => u.asMember(DiscordPlugin.mainServer.getId)).^^()
|
||||||
|
.flatMap(_.hasHigherRoles(Collections.singleton(r.getId)).^^().asInstanceOf).defaultIfEmpty(false) //Below one of our roles
|
||||||
|
else SMono.just(false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package buttondevteam.discordplugin.role
|
||||||
|
|
||||||
|
import buttondevteam.discordplugin.DiscordPlugin
|
||||||
|
import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC}
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI
|
||||||
|
import buttondevteam.lib.chat.{Command2, CommandClass}
|
||||||
|
import discord4j.core.`object`.entity.Role
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@CommandClass class RoleCommand private[role](var grm: GameRoleModule) extends ICommand2DC {
|
||||||
|
@Command2.Subcommand(helpText = Array(
|
||||||
|
"Add role",
|
||||||
|
"This command adds a role to your account."
|
||||||
|
)) def add(sender: Command2DCSender, @Command2.TextArg rolename: String): Boolean = {
|
||||||
|
val role = checkAndGetRole(sender, rolename)
|
||||||
|
if (role == null) return true
|
||||||
|
try sender.getMessage.getAuthorAsMember.flatMap(m => m.addRole(role.getId).switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("added role.")))).subscribe()
|
||||||
|
catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Error while adding role!", e, grm)
|
||||||
|
sender.sendMessage("an error occured while adding the role.")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand(helpText = Array(
|
||||||
|
"Remove role",
|
||||||
|
"This command removes a role from your account."
|
||||||
|
)) def remove(sender: Command2DCSender, @Command2.TextArg rolename: String): Boolean = {
|
||||||
|
val role = checkAndGetRole(sender, rolename)
|
||||||
|
if (role == null) return true
|
||||||
|
try sender.getMessage.getAuthorAsMember.flatMap(m => m.removeRole(role.getId).switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("removed role.")))).subscribe()
|
||||||
|
catch {
|
||||||
|
case e: Exception =>
|
||||||
|
TBMCCoreAPI.SendException("Error while removing role!", e, grm)
|
||||||
|
sender.sendMessage("an error occured while removing the role.")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand def list(sender: Command2DCSender): Unit = {
|
||||||
|
val sb = new StringBuilder
|
||||||
|
var b = false
|
||||||
|
for (role <- grm.GameRoles.stream.sorted.iterator.asInstanceOf[Iterable[String]]) {
|
||||||
|
sb.append(role)
|
||||||
|
if (!b) for (_ <- 0 until Math.max(1, 20 - role.length)) {
|
||||||
|
sb.append(" ")
|
||||||
|
}
|
||||||
|
else sb.append("\n")
|
||||||
|
b = !b
|
||||||
|
}
|
||||||
|
if (sb.nonEmpty && sb.charAt(sb.length - 1) != '\n') sb.append('\n')
|
||||||
|
sender.sendMessage("list of roles:\n```\n" + sb + "```")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def checkAndGetRole(sender: Command2DCSender, rolename: String): Role = {
|
||||||
|
var rname = rolename
|
||||||
|
if (!grm.GameRoles.contains(rolename)) { //If not found as-is, correct case
|
||||||
|
val orn = grm.GameRoles.stream.filter(r => r.equalsIgnoreCase(rolename)).findAny
|
||||||
|
if (!orn.isPresent) {
|
||||||
|
sender.sendMessage("that role cannot be found.")
|
||||||
|
list(sender)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
rname = orn.get
|
||||||
|
}
|
||||||
|
val frname = rname
|
||||||
|
val roles = DiscordPlugin.mainServer.getRoles.filter(r => r.getName.equals(frname)).collectList.block
|
||||||
|
if (roles == null) {
|
||||||
|
sender.sendMessage("an error occured.")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (roles.size == 0) {
|
||||||
|
sender.sendMessage("the specified role cannot be found on Discord! Removing from the list.")
|
||||||
|
grm.GameRoles.remove(rolename)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (roles.size > 1) {
|
||||||
|
sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
roles.get(0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package buttondevteam.discordplugin.util
|
||||||
|
|
||||||
|
object DPState extends Enumeration {
|
||||||
|
type DPState = Value
|
||||||
|
val
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used from server start until anything else happens
|
||||||
|
*/
|
||||||
|
RUNNING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when /restart is detected
|
||||||
|
*/
|
||||||
|
RESTARTING_SERVER,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when the plugin is disabled by outside forces
|
||||||
|
*/
|
||||||
|
STOPPING_SERVER,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when /discord restart is run
|
||||||
|
*/
|
||||||
|
RESTARTING_PLUGIN,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when the plugin is in the RUNNING state when the chat is disabled
|
||||||
|
*/
|
||||||
|
DISABLED_MCCHAT = Value
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue