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:
Norbi Peti 2022-01-05 01:35:34 +01:00
commit 6183c034c6
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
102 changed files with 4730 additions and 5568 deletions

451
.gitignore vendored
View file

@ -1,225 +1,226 @@
#################
## Eclipse
#################
*.pydevproject
.metadata/
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
target/
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.publishproj
# NuGet Packages Directory
## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
#############
## Windows detritus
#############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac crap
.DS_Store
#############
## Python
#############
*.py[cod]
# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
.metadata/*
TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
*.iml
*.name
.idea/compiler.xml
*.xml
Token.txt
#################
## Eclipse
#################
*.pydevproject
.metadata/
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
target/
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.publishproj
# NuGet Packages Directory
## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
#############
## Windows detritus
#############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac crap
.DS_Store
#############
## Python
#############
*.py[cod]
# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
.metadata/*
TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
*.iml
*.name
.idea/compiler.xml
*.xml
Token.txt
.bsp

141
build.sbt Normal file
View 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
View file

@ -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
View file

@ -0,0 +1,2 @@
sbt.version=1.5.8
scala.version=3.0.0

6
project/build.sbt Normal file
View 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
View file

@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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("");
}

View file

@ -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();
}
}

View file

@ -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));
}
}

View file

@ -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();
}

View file

@ -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();
}
}
}
}

View file

@ -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) {
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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() //
};
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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>"
};
}
}

View file

@ -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
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}
}

View file

@ -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&lt;DiscordID&gt; 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&lt;DiscordID&gt; 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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 didnt 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();
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -0,0 +1,6 @@
package buttondevteam.discordplugin
object ChannelconBroadcast extends Enumeration {
type ChannelconBroadcast = Value
val JOINLEAVE, AFK, RESTART, DEATH, BROADCAST = Value
}

View 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()
}

View 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 = ()
}
}

View file

@ -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
}*/
}

View file

@ -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)
}

View file

@ -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)
}
}

View 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 (&#39;&#39;), 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)
}
}
}

View file

@ -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 = ???
}

View file

@ -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: _*))
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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 =>
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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
}

View 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")
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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>")
}

View file

@ -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
}
}

View file

@ -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) {
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}
}
}
}

View file

@ -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&lt;DiscordID&gt; 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
}
}
}

View file

@ -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())
}
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -1,7 +1,5 @@
package buttondevteam.discordplugin.playerfaker;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
@ -16,8 +14,7 @@ import java.util.stream.IntStream;
public class DiscordInventory implements Inventory {
private ItemStack[] items = new ItemStack[27];
private List<ItemStack> itemStacks = Arrays.asList(items);
@Getter
@Setter
public int maxStackSize;
private static ItemStack emptyStack = new ItemStack(Material.AIR, 0);
@ -26,6 +23,16 @@ public class DiscordInventory implements Inventory {
return items.length;
}
@Override
public int getMaxStackSize() {
return maxStackSize;
}
@Override
public void setMaxStackSize(int maxStackSize) {
this.maxStackSize = maxStackSize;
}
@Override
public String getName() {
return "Discord inventory";

View file

@ -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 didnt 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)
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1,40 +1,9 @@
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.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
private final LPBukkitPlugin plugin;
/*private final LPBukkitPlugin plugin;
private final BukkitConnectionListener connectionListener;
private final Set<UUID> deniedLogin;
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
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent e) {
public void onPlayerLogin(PlayerLoginEvent e) {*/
/* Called when the player starts logging into the server.
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
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());
/* 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());
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);
}
}
}*/
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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