Merge pull request #99 from TBMCPlugins/dev

Updated to Discord4J v3, permission injection, improvements
This commit is contained in:
Norbi Peti 2019-06-06 22:45:21 +02:00 committed by GitHub
commit bc160a21b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2750 additions and 2422 deletions

447
pom.xml
View file

@ -1,208 +1,225 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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"> 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> <modelVersion>4.0.0</modelVersion>
<groupId>com.github.TBMCPlugins</groupId> <groupId>com.github.TBMCPlugins</groupId>
<artifactId>DiscordPlugin</artifactId> <artifactId>DiscordPlugin</artifactId>
<version>master-SNAPSHOT</version> <version>master-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>DiscordPlugin</name> <name>DiscordPlugin</name>
<url>http://maven.apache.org</url> <url>http://maven.apache.org</url>
<build> <build>
<!-- <sourceDirectory>target/generated-sources/delombok</sourceDirectory> <!-- <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
<testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> --> <testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> -->
<sourceDirectory>src/main/java</sourceDirectory> <sourceDirectory>src/main/java</sourceDirectory>
<resources> <resources>
<resource> <resource>
<directory>src</directory> <directory>src</directory>
<excludes> <excludes>
<exclude>**/*.java</exclude> <exclude>**/*.java</exclude>
</excludes> </excludes>
</resource> </resource>
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<includes> <includes>
<include>*.properties</include> <include>*.properties</include>
<include>*.yml</include> <include>*.yml</include>
<include>*.csv</include> <include>*.csv</include>
<include>*.txt</include> <include>*.txt</include>
</includes> </includes>
<filtering>true</filtering> <filtering>true</filtering>
</resource> </resource>
</resources> </resources>
<finalName>DiscordPlugin</finalName> <finalName>DiscordPlugin</finalName>
<plugins> <plugins>
<plugin> <plugin>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.6.2</version> <version>3.6.2</version>
<configuration> <configuration>
<source>1.8</source> <source>1.8</source>
<target>1.8</target> <target>1.8</target>
</configuration> <!-- <compilerArgs>
</plugin> <arg>-processor</arg>
<plugin> <arg>buttondevteam.buttonproc.ButtonProcessor, lombok.core.AnnotationProcessor</arg>
<groupId>org.apache.maven.plugins</groupId> </compilerArgs> -->
<artifactId>maven-shade-plugin</artifactId> <!-- <annotationProcessors>
<version>2.4.2</version> <annotationProcessor>lombok.launch.AnnotationProcessorHider$AnnotationProcessor</annotationProcessor>
<executions> <annotationProcessor>buttondevteam.buttonproc.ButtonProcessor</annotationProcessor>
<execution> </annotationProcessors> -->
<phase>package</phase> </configuration>
<goals> </plugin>
<goal>shade</goal> <plugin>
</goals> <groupId>org.apache.maven.plugins</groupId>
<configuration> <artifactId>maven-shade-plugin</artifactId>
<artifactSet> <version>2.4.2</version>
<excludes> <executions>
<exclude>org.spigotmc:spigot-api</exclude> <execution>
<exclude>com.github.TBMCPlugins.ButtonCore:ButtonCore</exclude> <phase>package</phase>
<exclude>net.ess3:Essentials</exclude> <goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>org.spigotmc:spigot-api</exclude>
<exclude>com.github.TBMCPlugins.ButtonCore:ButtonCore</exclude>
<exclude>net.ess3:Essentials</exclude>
</excludes> <!-- http://stackoverflow.com/questions/28458058/maven-shade-plugin-exclude-a-dependency-and-all-its-transitive-dependencies --> </excludes> <!-- http://stackoverflow.com/questions/28458058/maven-shade-plugin-exclude-a-dependency-and-all-its-transitive-dependencies -->
</artifactSet> </artifactSet>
</configuration> <minimizeJar>true</minimizeJar>
</execution> <relocations>
</executions> <relocation>
</plugin> <pattern>io.netty</pattern>
<plugin> <shadedPattern>btndvtm.dp.io.netty</shadedPattern>
<groupId>org.apache.maven.plugins</groupId> <excludes>
<artifactId>maven-resources-plugin</artifactId> </excludes>
<version>3.0.1</version> </relocation>
<executions> </relocations>
<execution> </configuration>
<id>copy</id> </execution>
<phase>compile</phase> </executions>
<goals> </plugin>
<goal>copy-resources</goal> <plugin>
</goals> <groupId>org.apache.maven.plugins</groupId>
<configuration> <artifactId>maven-resources-plugin</artifactId>
<outputDirectory>target</outputDirectory> <version>3.0.1</version>
<resources> <executions>
<resource> <execution>
<directory>resources</directory> <id>copy</id>
</resource> <phase>compile</phase>
</resources> <goals>
</configuration> <goal>copy-resources</goal>
</execution> </goals>
</executions> <configuration>
</plugin> <outputDirectory>target</outputDirectory>
<!-- <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId> <resources>
<version>1.16.16.0</version> <executions> <execution> <id>delombok</id> <phase>generate-sources</phase> <resource>
<goals> <goal>delombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory> <directory>resources</directory>
<sourceDirectory>src/main/java</sourceDirectory> <verbose>true</verbose> </resource>
</configuration> </execution> <execution> <id>test-delombok</id> <phase>generate-test-sources</phase> </resources>
<goals> <goal>testDelombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory> </configuration>
<sourceDirectory>src/test/java</sourceDirectory> </configuration> </execution> </execution>
</executions> </plugin> --> </executions>
<plugin> </plugin>
<artifactId>maven-surefire-plugin</artifactId> <!-- <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId>
<configuration> <version>1.16.16.0</version> <executions> <execution> <id>delombok</id> <phase>generate-sources</phase>
<useSystemClassLoader>false <goals> <goal>delombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory>
</useSystemClassLoader> <!-- https://stackoverflow.com/a/53012553/2703239 --> <sourceDirectory>src/main/java</sourceDirectory> <verbose>true</verbose>
</configuration> </configuration> </execution> <execution> <id>test-delombok</id> <phase>generate-test-sources</phase>
</plugin> <goals> <goal>testDelombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory>
</plugins> <sourceDirectory>src/test/java</sourceDirectory> </configuration> </execution>
</build> </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> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<branch> <branch>
master master
</branch> <!-- Should be master if building ButtonCore locally - the CI will overwrite it (see below) --> </branch> <!-- Should be master if building ButtonCore locally - the CI will overwrite it (see below) -->
</properties> </properties>
<repositories> <repositories>
<repository> <repository>
<id>spigot-repo</id> <id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url> <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository> </repository>
<repository> <!-- This repo fixes issues with transitive dependencies --> <repository> <!-- This repo fixes issues with transitive dependencies -->
<id>jcenter</id> <id>jcenter</id>
<url>http://jcenter.bintray.com</url> <url>http://jcenter.bintray.com</url>
</repository> </repository>
<repository> <repository>
<id>jitpack.io</id> <id>jitpack.io</id>
<url>https://jitpack.io</url> <url>https://jitpack.io</url>
</repository> </repository>
<!-- <repository> <!-- <repository>
<id>vault-repo</id> <id>vault-repo</id>
<url>http://nexus.hc.to/content/repositories/pub_releases</url> <url>http://nexus.hc.to/content/repositories/pub_releases</url>
</repository> --> </repository> -->
<repository> <repository>
<id>Essentials</id> <id>Essentials</id>
<url>http://repo.ess3.net/content/repositories/essrel/</url> <url>http://repo.ess3.net/content/repositories/essrel/</url>
</repository> </repository>
<repository> <repository>
<id>projectlombok.org</id> <id>projectlombok.org</id>
<url>http://projectlombok.org/mavenrepo</url> <url>http://projectlombok.org/mavenrepo</url>
</repository> </repository>
<!-- <repository> <!-- <repository>
<id>pex-repo</id> <id>pex-repo</id>
<url>http://pex-repo.aoeu.xyz</url> <url>http://pex-repo.aoeu.xyz</url>
</repository> --> </repository> -->
</repositories> <!-- <repository>
<id>Reactor-Tools</id>
<url>https://repo.spring.io/milestone</url>
</repository> -->
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>3.8.1</version> <version>3.8.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.12-R0.1-SNAPSHOT</version> <version>1.12-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId> <artifactId>spigot</artifactId>
<version>1.12.2-R0.1-SNAPSHOT</version> <version>1.12.2-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.discord4j/Discord4J --> <!-- https://mvnrepository.com/artifact/com.discord4j/Discord4J -->
<dependency> <dependency>
<groupId>com.discord4j</groupId> <groupId>com.discord4j</groupId>
<artifactId>Discord4J</artifactId> <artifactId>discord4j-core</artifactId>
<version>2.10.1</version> <version>3.0.6</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 --> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId> <artifactId>slf4j-jdk14</artifactId>
<version>1.7.21</version> <version>1.7.21</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.TBMCPlugins.ButtonCore</groupId> <groupId>com.github.TBMCPlugins.ButtonCore</groupId>
<artifactId>ButtonCore</artifactId> <artifactId>ButtonCore</artifactId>
<version>${branch}-SNAPSHOT</version> <version>${branch}-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.milkbowl</groupId> <!-- net.milkbowl.vault -->
<artifactId>VaultAPI</artifactId>
<version>master-SNAPSHOT</version> <!-- 1.6 -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.ess3</groupId>
<artifactId>Essentials</artifactId>
<version>2.13.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.xaanit</groupId> <groupId>com.github.milkbowl</groupId> <!-- net.milkbowl.vault -->
<artifactId>D4J-OAuth</artifactId> <artifactId>VaultAPI</artifactId>
<version>master-SNAPSHOT</version> <version>master-SNAPSHOT</version> <!-- 1.6 -->
</dependency> <scope>provided</scope>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>net.ess3</groupId>
<artifactId>lombok</artifactId> <artifactId>Essentials</artifactId>
<version>1.16.16</version> <version>2.13.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>
<!-- <dependency> <!-- <dependency>
<groupId>ru.tehkode</groupId> <groupId>ru.tehkode</groupId>
<artifactId>PermissionsEx</artifactId> <artifactId>PermissionsEx</artifactId>
@ -215,32 +232,44 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> --> </dependency> -->
<!-- https://mvnrepository.com/artifact/org.objenesis/objenesis --> <!-- https://mvnrepository.com/artifact/org.objenesis/objenesis -->
<dependency> <dependency>
<groupId>org.objenesis</groupId> <groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId> <artifactId>objenesis</artifactId>
<version>2.6</version> <version>2.6</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.vdurmont</groupId> <groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId> <artifactId>emoji-java</artifactId>
<version>4.0.0</version> <version>4.0.0</version>
</dependency> </dependency>
</dependencies> <!-- 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</groupId>
<artifactId>LuckPerms</artifactId>
<version>v4.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<profiles> <profiles>
<profile> <profile>
<id>ci</id> <id>ci</id>
<activation> <activation>
<property> <property>
<name>env.TRAVIS_BRANCH</name> <name>env.TRAVIS_BRANCH</name>
</property> </property>
</activation> </activation>
<properties> <properties>
<!-- Override only if necessary --> <!-- Override only if necessary -->
<branch>${env.TRAVIS_BRANCH}</branch> <branch>${env.TRAVIS_BRANCH}</branch>
</properties> </properties>
</profile> </profile>
</profiles> </profiles>
</project> </project>

View file

@ -1,27 +0,0 @@
package buttondevteam.discordplugin;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@RequiredArgsConstructor
public class AsyncDiscordEvent<T extends sx.blah.discord.api.events.Event> extends Event implements Cancellable {
private final @Getter T event;
@Getter
@Setter
private boolean cancelled;
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View file

@ -1,15 +1,14 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.discordplugin.mcchat.MCChatUtils;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel;
import lombok.Getter; import lombok.Getter;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitScheduler;
import sx.blah.discord.api.internal.json.objects.EmbedObject; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.util.EmbedBuilder;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.awt.*; import java.util.function.Function;
public class ChromaBot { public class ChromaBot {
/** /**
@ -33,113 +32,26 @@ public class ChromaBot {
instance = null; instance = null;
} }
/**
* Send a message to the chat channel and private chats.
*
* @param message
* The message to send, duh
*/
public void sendMessage(String message) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message));
}
/** /**
* Send a message to the chat channels and private chats. * Send a message to the chat channels and private chats.
* *
* @param message * @param message
* The message to send, duh * The message to send, duh (use {@link MessageChannel#createMessage(String)})
* @param embed
* Custom fancy stuff, use {@link EmbedBuilder} to create one
*/ */
public void sendMessage(String message, EmbedObject embed) { public void sendMessage(Function<Mono<MessageChannel>, Mono<Message>> message) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed)); MCChatUtils.forAllMCChat(ch -> message.apply(ch).subscribe());
} }
/** /**
* Send a message to the chat channels, private chats and custom chats. * Send a message to the chat channels, private chats and custom chats.
* *
* @param message The message to send, duh * @param message The message to send, duh
* @param embed Custom fancy stuff, use {@link EmbedBuilder} to create one
* @param toggle The toggle type for channelcon * @param toggle The toggle type for channelcon
*/ */
public void sendMessageCustomAsWell(String message, EmbedObject embed, @Nullable ChannelconBroadcast toggle) { public void sendMessageCustomAsWell(Function<Mono<MessageChannel>, Mono<Message>> message, @Nullable ChannelconBroadcast toggle) {
MCChatUtils.forCustomAndAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed), toggle, false); MCChatUtils.forCustomAndAllMCChat(ch -> message.apply(ch).subscribe(), toggle, false);
} }
/**
* Send a message to an arbitrary channel. This will not send it to the private chats.
*
* @param channel
* The channel to send to, use the channel variables in {@link DiscordPlugin}
* @param message
* The message to send, duh
* @param embed
* Custom fancy stuff, use {@link EmbedBuilder} to create one
*/
public void sendMessage(IChannel channel, String message, EmbedObject embed) {
DiscordPlugin.sendMessageToChannel(channel, message, embed);
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
*/
public void sendMessage(String message, Color color) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message,
new EmbedBuilder().withTitle(message).withColor(color).build()));
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param mcauthor
* The name of the Minecraft player who is the author of this message
*/
public void sendMessage(String message, Color color, String mcauthor) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message,
DPUtils.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), mcauthor).build()));
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param authorname
* The name of the author of this message
* @param authorimg
* The URL of the avatar image for this message's author
*/
public void sendMessage(String message, Color color, String authorname, String authorimg) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, new EmbedBuilder()
.withTitle(message).withColor(color).withAuthorName(authorname).withAuthorIcon(authorimg).build()));
}
/**
* Send a message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param sender
* The player who sends this message
*/
public void sendMessage(String message, Color color, Player sender) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, DPUtils
.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), sender.getName()).build()));
}
public void updatePlayerList() { public void updatePlayerList() {
MCChatUtils.updatePlayerList(); MCChatUtils.updatePlayerList();
} }

View file

@ -4,128 +4,82 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.IHaveConfig; import buttondevteam.lib.architecture.IHaveConfig;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.Role;
import discord4j.core.object.util.Snowflake;
import discord4j.core.spec.EmbedCreateSpec;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IIDLinkedObject;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.RequestBuffer;
import sx.blah.discord.util.RequestBuffer.IRequest;
import sx.blah.discord.util.RequestBuffer.IVoidRequest;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
public final class DPUtils { public final class DPUtils {
public static EmbedBuilder embedWithHead(EmbedBuilder builder, String playername) { public static EmbedCreateSpec embedWithHead(EmbedCreateSpec ecs, String displayname, String playername, String profileUrl) {
return builder.withAuthorIcon("https://minotar.net/avatar/" + playername + "/32.png"); 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) {
String sanitizedString = "";
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 += string.charAt(i);
}
}
return sanitizedString;
}
/** /**
* Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode. * Removes §[char] colour codes from strings & escapes them for Discord <br>
* Ensure that this method only gets called once (escaping)
*/ */
@Nullable public static String sanitizeString(String string) {
public static <T> T perform(IRequest<T> action, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { return escape(sanitizeStringNoEscape(string));
if (DiscordPlugin.SafeMode) }
return null;
if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <--
// throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag.");
getLogger().warning("Waiting for a Discord request on the main thread!");
return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this
}
/**
* Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode.
*/
@Nullable
public static <T> T perform(IRequest<T> action) {
if (DiscordPlugin.SafeMode)
return null;
if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <--
// throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag.");
getLogger().warning("Waiting for a Discord request on the main thread!");
return RequestBuffer.request(action).get(); // Let the pros handle this
}
/** /**
* Performs Discord actions, retrying when ratelimited. * Removes §[char] colour codes from strings
*/ */
public static Void perform(IVoidRequest action) { public static String sanitizeStringNoEscape(String string) {
if (DiscordPlugin.SafeMode) StringBuilder sanitizedString = new StringBuilder();
return null; boolean random = false;
if (Bukkit.isPrimaryThread()) for (int i = 0; i < string.length(); i++) {
throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); if (string.charAt(i) == '§') {
return RequestBuffer.request(action).get(); // Let the pros handle this 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();
} }
public static void performNoWait(IVoidRequest action) { private static String escape(String message) {
if (DiscordPlugin.SafeMode) return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\") + "$1");
return;
RequestBuffer.request(action);
} }
public static <T> void performNoWait(IRequest<T> action) {
if (DiscordPlugin.SafeMode)
return;
RequestBuffer.request(action);
}
public static String escape(String message) {
return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\")+"$1");
}
public static Logger getLogger() { public static Logger getLogger() {
if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null) if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null)
return Logger.getLogger("DiscordPlugin"); return Logger.getLogger("DiscordPlugin");
return DiscordPlugin.plugin.getLogger(); return DiscordPlugin.plugin.getLogger();
} }
public static ConfigData<IChannel> channelData(IHaveConfig config, String key, long defID) { public static ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key, long defID) {
return config.getDataPrimDef(key, defID, id -> DiscordPlugin.dc.getChannelByID((long) id), IIDLinkedObject::getLongID); //We can afford to search for the channel in the cache once (instead of using mainServer) return config.getReadOnlyDataPrimDef(key, defID, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> defID); //We can afford to search for the channel in the cache once (instead of using mainServer)
} }
public static ConfigData<IRole> roleData(IHaveConfig config, String key, String defName) { public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName) {
return roleData(config, key, defName, DiscordPlugin.mainServer); return roleData(config, key, defName, Mono.just(DiscordPlugin.mainServer));
} }
public static ConfigData<IRole> roleData(IHaveConfig config, String key, String defName, IGuild guild) { /**
return config.getDataPrimDef(key, defName, name -> { * Needs to be a {@link ConfigData} for checking if it's set
if (!(name instanceof String)) return null; */
val roles = guild.getRolesByName((String) name); public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName, Mono<Guild> guild) {
return roles.size() > 0 ? roles.get(0) : null; return config.getReadOnlyDataPrimDef(key, defName, name -> {
}, IIDLinkedObject::getLongID); if (!(name instanceof String)) return Mono.empty();
return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).next();
}, r -> defName);
}
public static ConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) {
return config.getDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong);
} }
/** /**
@ -134,10 +88,8 @@ public final class DPUtils {
* @return The string for mentioning the channel * @return The string for mentioning the channel
*/ */
public static String botmention() { public static String botmention() {
IChannel channel; if (DiscordPlugin.plugin == null) return "#bot";
if (DiscordPlugin.plugin == null return channelMention(DiscordPlugin.plugin.commandChannel().get());
|| (channel = DiscordPlugin.plugin.CommandChannel().get()) == null) return "#bot";
return channel.mention();
} }
/** /**
@ -149,23 +101,66 @@ public final class DPUtils {
*/ */
public static boolean disableIfConfigError(@Nullable Component<DiscordPlugin> component, ConfigData<?>... configs) { public static boolean disableIfConfigError(@Nullable Component<DiscordPlugin> component, ConfigData<?>... configs) {
for (val config : configs) { for (val config : configs) {
if (config.get() == null) { Object v = config.get();
String path = null; if (disableIfConfigErrorRes(component, config, v))
try {
if (component != null)
Component.setComponentEnabled(component, false);
val f = ConfigData.class.getDeclaredField("path");
f.setAccessible(true); //Hacking my own plugin
path = (String) f.get(config);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to disable component after config error!", e);
}
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 true;
}
} }
return false; 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) {
TBMCCoreAPI.SendException("Failed to disable component after config error!", e);
}
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;
}
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 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() + ">";
}
public static Mono<MessageChannel> getMessageChannel(String key, Snowflake id) {
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());
}
} }

View file

@ -1,19 +1,24 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer; import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import lombok.Getter; import lombok.Getter;
import sx.blah.discord.handle.obj.IChannel; import lombok.Setter;
import sx.blah.discord.handle.obj.IUser;
import java.util.UUID; import java.util.UUID;
public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer<DiscordConnectedPlayer> { public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer<DiscordConnectedPlayer> {
private static int nextEntityId = 10000; private static int nextEntityId = 10000;
private @Getter VanillaCommandListener<DiscordConnectedPlayer> vanillaCmdListener; private @Getter VanillaCommandListener<DiscordConnectedPlayer> vanillaCmdListener;
@Getter
@Setter
private boolean loggedIn = false;
public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid, String mcname) { public DiscordConnectedPlayer(User user, MessageChannel channel, UUID uuid, String mcname, MinecraftChatModule module) {
super(user, channel, nextEntityId++, uuid, mcname); super(user, channel, nextEntityId++, uuid, mcname, module);
vanillaCmdListener = new VanillaCommandListener<>(this); vanillaCmdListener = new VanillaCommandListener<>(this);
} }

View file

@ -1,28 +1,28 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import buttondevteam.discordplugin.mcchat.MCChatPrivate; import buttondevteam.discordplugin.mcchat.MCChatPrivate;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.UserClass; import buttondevteam.lib.player.UserClass;
@UserClass(foldername = "discord") @UserClass(foldername = "discord")
public class DiscordPlayer extends ChromaGamerBase { public class DiscordPlayer extends ChromaGamerBase {
private String did; private String did;
// private @Getter @Setter boolean minecraftChatEnabled; // private @Getter @Setter boolean minecraftChatEnabled;
public DiscordPlayer() { public DiscordPlayer() {
} }
public String getDiscordID() { public String getDiscordID() {
if (did == null) if (did == null)
did = plugindata.getString(getFolder() + "_id"); did = plugindata.getString(getFolder() + "_id");
return did; return did;
} }
/** /**
* Returns true if player has the private Minecraft chat enabled. For setting the value, see * Returns true if player has the private Minecraft chat enabled. For setting the value, see
* {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.IChannel, boolean, sx.blah.discord.handle.obj.IUser, DiscordPlayer)} * {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.MessageChannel, boolean, sx.blah.discord.handle.obj.User, DiscordPlayer)}
*/ */
public boolean isMinecraftChatEnabled() { public boolean isMinecraftChatEnabled() {
return MCChatPrivate.isMinecraftChatEnabled(this); return MCChatPrivate.isMinecraftChatEnabled(this);
} }
} }

View file

@ -1,6 +1,8 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import lombok.Getter; import lombok.Getter;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.advancement.Advancement; import org.bukkit.advancement.Advancement;
@ -26,8 +28,6 @@ import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import org.bukkit.scoreboard.Scoreboard; import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.*; import java.util.*;
@ -38,7 +38,7 @@ public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer<
protected Player player; protected Player player;
private @Getter VanillaCommandListener<DiscordPlayerSender> vanillaCmdListener; private @Getter VanillaCommandListener<DiscordPlayerSender> vanillaCmdListener;
public DiscordPlayerSender(IUser user, IChannel channel, Player player) { public DiscordPlayerSender(User user, MessageChannel channel, Player player) {
super(user, channel); super(user, channel);
this.player = player; this.player = player;
vanillaCmdListener = new VanillaCommandListener<DiscordPlayerSender>(this); vanillaCmdListener = new VanillaCommandListener<DiscordPlayerSender>(this);

View file

@ -10,16 +10,27 @@ import buttondevteam.discordplugin.listeners.MCListener;
import buttondevteam.discordplugin.mcchat.MCChatPrivate; import buttondevteam.discordplugin.mcchat.MCChatPrivate;
import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.mccommands.DiscordMCCommandBase; import buttondevteam.discordplugin.mccommands.DiscordMCCommand;
import buttondevteam.discordplugin.mccommands.ResetMCCommand;
import buttondevteam.discordplugin.role.GameRoleModule; import buttondevteam.discordplugin.role.GameRoleModule;
import buttondevteam.discordplugin.util.Timings;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.architecture.IHaveConfig;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import com.google.common.io.Files; import com.google.common.io.Files;
import discord4j.core.DiscordClient;
import discord4j.core.DiscordClientBuilder;
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.core.object.util.Snowflake;
import discord4j.store.jdk.JdkStoreService;
import lombok.Getter; import lombok.Getter;
import lombok.val; import lombok.val;
import net.milkbowl.vault.permission.Permission; import net.milkbowl.vault.permission.Permission;
@ -27,310 +38,254 @@ import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.scheduler.BukkitTask; import reactor.core.publisher.Mono;
import sx.blah.discord.api.ClientBuilder;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.api.events.IListener;
import sx.blah.discord.api.internal.json.objects.EmbedObject;
import sx.blah.discord.handle.impl.events.ReadyEvent;
import sx.blah.discord.handle.impl.obj.ReactionEmoji;
import sx.blah.discord.handle.obj.*;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.RequestBuffer;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class DiscordPlugin extends ButtonPlugin implements IListener<ReadyEvent> { @ButtonPlugin.ConfigOpts(disableConfigGen = true)
public static IDiscordClient dc; public class DiscordPlugin extends ButtonPlugin {
public static DiscordPlugin plugin; public static DiscordClient dc;
public static boolean SafeMode = true; public static DiscordPlugin plugin;
public static boolean SafeMode = true;
@Getter @Getter
private Command2DC manager; private Command2DC manager;
public ConfigData<Character> Prefix() { private ConfigData<Character> prefix() {
return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString); return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString);
}
public static char getPrefix() {
if (plugin == null) return '/';
return plugin.Prefix().get();
}
public ConfigData<IGuild> MainServer() {
return getIConfig().getDataPrimDef("mainServer", 219529124321034241L, id -> dc.getGuildByID((long) id), IIDLinkedObject::getLongID);
} }
public ConfigData<IChannel> CommandChannel() { public static char getPrefix() {
return DPUtils.channelData(getIConfig(), "commandChannel", 239519012529111040L); if (plugin == null) return '/';
return plugin.prefix().get();
} }
public ConfigData<IRole> ModRole() { 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));
}
public ConfigData<Snowflake> commandChannel() {
return DPUtils.snowflakeData(getIConfig(), "commandChannel", 239519012529111040L);
}
/**
* If the role doesn't exist, then it will only allow for the owner.
*/
public ConfigData<Mono<Role>> modRole() {
return DPUtils.roleData(getIConfig(), "modRole", "Moderator"); return DPUtils.roleData(getIConfig(), "modRole", "Moderator");
} }
@Override /**
public void pluginEnable() { * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
try { */
getLogger().info("Initializing..."); public ConfigData<String> inviteLink() {
plugin = this; return getIConfig().getData("inviteLink", "");
manager = new Command2DC(); }
ClientBuilder cb = new ClientBuilder();
File tokenFile = new File("TBMC", "Token.txt");
if (tokenFile.exists()) //Legacy support
//noinspection UnstableApiUsage
cb.withToken(Files.readFirstLine(tokenFile, StandardCharsets.UTF_8));
else {
File privateFile = new File(getDataFolder(), "private.yml");
val conf = YamlConfiguration.loadConfiguration(privateFile);
String token = conf.getString("token");
if (token == null) {
conf.set("token", "Token goes here");
conf.save(privateFile);
getLogger().severe("Token not found! Set it in private.yml"); @Override
Bukkit.getPluginManager().disablePlugin(this); public void pluginEnable() {
return; try {
} else getLogger().info("Initializing...");
cb.withToken(token); plugin = this;
} manager = new Command2DC();
dc = cb.login(); String token;
dc.getDispatcher().registerListener(this); File tokenFile = new File("TBMC", "Token.txt");
} catch (Exception e) { if (tokenFile.exists()) //Legacy support
e.printStackTrace(); //noinspection UnstableApiUsage
Bukkit.getPluginManager().disablePlugin(this); 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);
public static IGuild mainServer; getLogger().severe("Token not found! Set it in private.yml");
Bukkit.getPluginManager().disablePlugin(this);
return;
}
}
val cb = new DiscordClientBuilder(token);
cb.setInitialPresence(Presence.doNotDisturb(Activity.playing("booting")));
cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now
dc = cb.build();
dc.getEventDispatcher().on(ReadyEvent.class) // Listen for ReadyEvent(s)
.map(event -> event.getGuilds().size()) // Get how many guilds the bot is in
.flatMap(size -> dc.getEventDispatcher()
.on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s)
.take(size) // Take only the first `size` GuildCreateEvent(s) to be received
.collectList()) // Take all received GuildCreateEvents and make it a List
.subscribe(this::handleReady); /* All guilds have been received, client is fully connected */
dc.login().subscribe();
} catch (Exception e) {
e.printStackTrace();
Bukkit.getPluginManager().disablePlugin(this);
}
}
private static volatile BukkitTask task; public static Guild mainServer;
private static volatile boolean sent = false;
@Override private void handleReady(List<GuildCreateEvent> event) {
public void handle(ReadyEvent event) { try {
try { mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards
dc.changePresence(StatusType.DND, ActivityType.PLAYING, "booting"); if (mainServer == null) {
val tries = new AtomicInteger(); if (event.size() == 0) {
task = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { getLogger().severe("Main server not found! Invite the bot and do /discord reset");
tries.incrementAndGet(); saveConfig(); //Put default there
if (tries.get() > 10) { //5 seconds return; //We should have all guilds by now, no need to retry
task.cancel(); }
getLogger().severe("Main server not found! Invite the bot and do /discord reset"); mainServer = event.get(0).getGuild();
//getIConfig().getConfig().set("mainServer", 219529124321034241L); //Needed because it won't save as long as it's null - made it save getLogger().warning("Main server set to first one: " + mainServer.getName());
saveConfig(); //Put default there mainServer().set(Optional.of(mainServer)); //Save in config
return; }
} SafeMode = false;
mainServer = MainServer().get(); //Shouldn't change afterwards DPUtils.disableIfConfigErrorRes(null, commandChannel(), DPUtils.getMessageChannel(commandChannel()));
if (mainServer == null) { DPUtils.disableIfConfigError(null, modRole()); //Won't disable, just prints the warning here
val guilds = dc.getGuilds();
if (guilds.size() == 0)
return; //If there are no guilds in cache, retry
mainServer = guilds.get(0);
getLogger().warning("Main server set to first one: " + mainServer.getName());
MainServer().set(mainServer); //Save in config
}
if (!TBMCCoreAPI.IsTestServer()) { //Don't change conditions here, see mainServer=devServer=null in onDisable()
dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "Minecraft");
} else {
dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "testing");
}
SafeMode = false;
if (task != null)
task.cancel();
if (!sent) {
DPUtils.disableIfConfigError(null, CommandChannel(), ModRole()); //Won't disable, just prints the warning here
Component.registerComponent(this, new GeneralEventBroadcasterModule()); Component.registerComponent(this, new GeneralEventBroadcasterModule());
Component.registerComponent(this, new MinecraftChatModule()); Component.registerComponent(this, new MinecraftChatModule());
Component.registerComponent(this, new ExceptionListenerModule()); Component.registerComponent(this, new ExceptionListenerModule());
Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set
Component.registerComponent(this, new AnnouncerModule()); Component.registerComponent(this, new AnnouncerModule());
Component.registerComponent(this, new FunModule()); Component.registerComponent(this, new FunModule());
new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled
getManager().registerCommand(new VersionCommand()); getManager().registerCommand(new VersionCommand());
getManager().registerCommand(new UserinfoCommand()); getManager().registerCommand(new UserinfoCommand());
getManager().registerCommand(new HelpCommand()); getManager().registerCommand(new HelpCommand());
getManager().registerCommand(new DebugCommand()); getManager().registerCommand(new DebugCommand());
getManager().registerCommand(new ConnectCommand()); getManager().registerCommand(new ConnectCommand());
if (ResetMCCommand.resetting) //These will only execute if the chat is enabled if (DiscordMCCommand.resetting) //These will only execute if the chat is enabled
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.CYAN) ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.CYAN)
.withTitle("Discord plugin restarted - chat connected.").build(), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm .setTitle("Discord plugin restarted - chat connected."))), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm
else if (getConfig().getBoolean("serverup", false)) { else if (getConfig().getBoolean("serverup", false)) {
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.YELLOW) ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.YELLOW)
.withTitle("Server recovered from a crash - chat connected.").build(), ChannelconBroadcast.RESTART); .setTitle("Server recovered from a crash - chat connected."))), ChannelconBroadcast.RESTART);
val thr = new Throwable( val thr = new Throwable(
"The server shut down unexpectedly. See the log of the previous run for more details."); "The server shut down unexpectedly. See the log of the previous run for more details.");
thr.setStackTrace(new StackTraceElement[0]); thr.setStackTrace(new StackTraceElement[0]);
TBMCCoreAPI.SendException("The server crashed!", thr); TBMCCoreAPI.SendException("The server crashed!", thr);
} else } else
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.GREEN) ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.GREEN)
.withTitle("Server started - chat connected.").build(), ChannelconBroadcast.RESTART); .setTitle("Server started - chat connected."))), ChannelconBroadcast.RESTART);
ResetMCCommand.resetting = false; //This is the last event handling this flag DiscordMCCommand.resetting = false; //This is the last event handling this flag
getConfig().set("serverup", true); getConfig().set("serverup", true);
saveConfig(); saveConfig();
sent = true; if (TBMCCoreAPI.IsTestServer() && !Objects.requireNonNull(dc.getSelf().block()).getUsername().toLowerCase().contains("test")) {
if (TBMCCoreAPI.IsTestServer() && !dc.getOurUser().getName().toLowerCase().contains("test")) { TBMCCoreAPI.SendException(
TBMCCoreAPI.SendException( "Won't load because we're in testing mode and not using a separate account.",
"Won't load because we're in testing mode and not using a separate account.", new Exception(
new Exception( "The plugin refuses to load until you change the token to a testing account. (The account needs to have \"test\" in its name.)"
"The plugin refuses to load until you change the token to a testing account. (The account needs to have \"test\" in it's name.)")); + "\nYou can disable test mode in ThorpeCore config."));
Bukkit.getPluginManager().disablePlugin(this); Bukkit.getPluginManager().disablePlugin(this);
} }
TBMCCoreAPI.SendUnsentExceptions(); TBMCCoreAPI.SendUnsentExceptions();
TBMCCoreAPI.SendUnsentDebugMessages(); TBMCCoreAPI.SendUnsentDebugMessages();
}
}, 0, 10); CommonListeners.register(dc.getEventDispatcher());
for (IListener<?> listener : CommonListeners.getListeners()) TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this);
dc.getDispatcher().registerListener(listener); getCommand2MC().registerCommand(new DiscordMCCommand());
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class);
TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class); ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase
TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); ? ((DiscordSenderBase) sender).getChromaUser() : null));
ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase setupProviders();
? ((DiscordSenderBase) sender).getChromaUser() : null));
setupProviders(); IHaveConfig.pregenConfig(this, null);
} catch (Exception e) { if (!TBMCCoreAPI.IsTestServer()) {
TBMCCoreAPI.SendException("An error occured while enabling DiscordPlugin!", e); dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe();
} } else {
} dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe();
}
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e);
}
}
/** /**
* Always true, except when running "stop" from console * Always true, except when running "stop" from console
*/ */
public static boolean Restart; public static boolean Restart;
@Override @Override
public void pluginPreDisable() { public void pluginPreDisable() {
if (ChromaBot.getInstance() == null) return; //Failed to load if (ChromaBot.getInstance() == null) return; //Failed to load
EmbedObject embed; Timings timings = new Timings();
if (ResetMCCommand.resetting) timings.printElapsed("Disable start");
embed = new EmbedBuilder().withColor(Color.ORANGE).withTitle("Discord plugin restarting").build(); MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> {
else timings.printElapsed("Sending message to " + ch.getMention());
embed = new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) if (DiscordMCCommand.resetting)
.withTitle(Restart ? "Server restarting" : "Server stopping") ecs.setColor(Color.ORANGE).setTitle("Discord plugin restarting");
.withDescription( else
Bukkit.getOnlinePlayers().size() > 0 ecs.setColor(Restart ? Color.ORANGE : Color.RED)
? (DPUtils .setTitle(Restart ? "Server restarting" : "Server stopping")
.sanitizeString(Bukkit.getOnlinePlayers().stream() .setDescription(
.map(Player::getDisplayName).collect(Collectors.joining(", "))) Bukkit.getOnlinePlayers().size() > 0
+ (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") ? (DPUtils
+ "kicked the hell out.") //TODO: Make configurable .sanitizeString(Bukkit.getOnlinePlayers().stream()
: "") //If 'restart' is disabled then this isn't shown even if joinleave is enabled .map(Player::getDisplayName).collect(Collectors.joining(", ")))
.build(); + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ")
MCChatUtils.forCustomAndAllMCChat(ch -> { + "kicked the hell out.") //TODO: Make configurable
try { : ""); //If 'restart' is disabled then this isn't shown even if joinleave is enabled
DiscordPlugin.sendMessageToChannelWait(ch, "", })).subscribe(), ChannelconBroadcast.RESTART, false);
embed, 5, TimeUnit.SECONDS); timings.printElapsed("Updating player list");
} catch (TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}, ChannelconBroadcast.RESTART, false);
ChromaBot.getInstance().updatePlayerList(); ChromaBot.getInstance().updatePlayerList();
timings.printElapsed("Done");
} }
@Override @Override
public void pluginDisable() { public void pluginDisable() {
Timings timings = new Timings();
timings.printElapsed("Actual disable start (logout)");
MCChatPrivate.logoutAll(); MCChatPrivate.logoutAll();
timings.printElapsed("Config setup");
getConfig().set("serverup", false); getConfig().set("serverup", false);
if (ChromaBot.getInstance() == null) return; //Failed to load if (ChromaBot.getInstance() == null) return; //Failed to load
saveConfig(); saveConfig();
try { try {
SafeMode = true; // Stop interacting with Discord SafeMode = true; // Stop interacting with Discord
ChromaBot.delete(); ChromaBot.delete();
dc.changePresence(StatusType.IDLE, ActivityType.PLAYING, "Chromacraft"); //No longer using the same account for testing timings.printElapsed("Updating presence...");
dc.logout(); dc.updatePresence(Presence.idle(Activity.playing("Chromacraft"))).block(); //No longer using the same account for testing
//Configs are emptied so channels and servers are fetched again timings.printElapsed("Logging out...");
sent = false; dc.logout().block();
} catch (Exception e) { //Configs are emptied so channels and servers are fetched again
TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e); } catch (Exception e) {
} TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e);
} }
}
public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.of(""); public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("");
public static void sendMessageToChannel(IChannel channel, String message) { public static Permission perms;
sendMessageToChannel(channel, message, null);
}
public static void sendMessageToChannel(IChannel channel, String message, EmbedObject embed) { private boolean setupProviders() {
try { try {
sendMessageToChannel(channel, message, embed, false); Class.forName("net.milkbowl.vault.permission.Permission");
} catch (TimeoutException | InterruptedException e) { Class.forName("net.milkbowl.vault.chat.Chat");
e.printStackTrace(); //Shouldn't happen, as we're not waiting on the result } catch (ClassNotFoundException e) {
} return false;
} }
public static IMessage sendMessageToChannelWait(IChannel channel, String message) throws TimeoutException, InterruptedException { RegisteredServiceProvider<Permission> permsProvider = Bukkit.getServer().getServicesManager()
return sendMessageToChannelWait(channel, message, null); .getRegistration(Permission.class);
} perms = permsProvider.getProvider();
return perms != null;
public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed) throws TimeoutException, InterruptedException { }
return sendMessageToChannel(channel, message, embed, true);
}
public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
return sendMessageToChannel(channel, message, embed, true, timeout, unit);
}
private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait) throws TimeoutException, InterruptedException {
return sendMessageToChannel(channel, message, embed, wait, -1, null);
}
private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
if (message.length() > 1980) {
message = message.substring(0, 1980);
DPUtils.getLogger()
.warning("Message was too long to send to discord and got truncated. In " + channel.getName());
}
try {
MCChatUtils.resetLastMessage(channel); // If this is a chat message, it'll be set again
final String content = message;
RequestBuffer.IRequest<IMessage> r = () -> embed == null ? channel.sendMessage(content)
: channel.sendMessage(content, embed, false);
if (wait) {
if (unit != null)
return DPUtils.perform(r, timeout, unit);
else
return DPUtils.perform(r);
} else {
if (unit != null)
plugin.getLogger().warning("Tried to set timeout for non-waiting call.");
else
DPUtils.performNoWait(r);
return null;
}
} catch (TimeoutException | InterruptedException e) {
throw e;
} catch (Exception e) {
DPUtils.getLogger().warning(
"Failed to deliver message to Discord! Channel: " + channel.getName() + " Message: " + message);
throw new RuntimeException(e);
}
}
public static Permission perms;
public boolean setupProviders() {
try {
Class.forName("net.milkbowl.vault.permission.Permission");
Class.forName("net.milkbowl.vault.chat.Chat");
} catch (ClassNotFoundException e) {
return false;
}
RegisteredServiceProvider<Permission> permsProvider = Bukkit.getServer().getServicesManager()
.getRegistration(Permission.class);
perms = permsProvider.getProvider();
return perms != null;
}
} }

View file

@ -1,10 +0,0 @@
package buttondevteam.discordplugin;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
import sx.blah.discord.util.RateLimitException;
@FunctionalInterface
public interface DiscordRunnable {
public abstract void run() throws DiscordException, RateLimitException, MissingPermissionsException;
}

View file

@ -1,5 +1,9 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -8,8 +12,6 @@ import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.Set; import java.util.Set;
@ -18,12 +20,13 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender {
private String name; private String name;
public DiscordSender(IUser user, IChannel channel) { public DiscordSender(User user, MessageChannel channel) {
super(user, channel); super(user, channel);
name = user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer); val def = "Discord user";
name = user == null ? def : user.asMember(DiscordPlugin.mainServer.getId()).blockOptional().map(Member::getDisplayName).orElse(def);
} }
public DiscordSender(IUser user, IChannel channel, String name) { public DiscordSender(User user, MessageChannel channel, String name) {
super(user, channel); super(user, channel);
this.name = name; this.name = name;
} }

View file

@ -1,20 +1,20 @@
package buttondevteam.discordplugin; package buttondevteam.discordplugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
public abstract class DiscordSenderBase implements CommandSender { public abstract class DiscordSenderBase implements CommandSender {
/** /**
* May be null. * May be null.
*/ */
protected IUser user; protected User user;
protected IChannel channel; protected MessageChannel channel;
protected DiscordSenderBase(IUser user, IChannel channel) { protected DiscordSenderBase(User user, MessageChannel channel) {
this.user = user; this.user = user;
this.channel = channel; this.channel = channel;
} }
@ -27,11 +27,11 @@ public abstract class DiscordSenderBase implements CommandSender {
* *
* @return The user or null. * @return The user or null.
*/ */
public IUser getUser() { public User getUser() {
return user; return user;
} }
public IChannel getChannel() { public MessageChannel getChannel() {
return channel; return channel;
} }
@ -43,7 +43,7 @@ public abstract class DiscordSenderBase implements CommandSender {
* @return A Chroma user of Discord or a Discord user of Chroma * @return A Chroma user of Discord or a Discord user of Chroma
*/ */
public DiscordPlayer getChromaUser() { public DiscordPlayer getChromaUser() {
if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getStringID(), DiscordPlayer.class); if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getId().asString(), DiscordPlayer.class);
return chromaUser; return chromaUser;
} }
@ -58,8 +58,7 @@ public abstract class DiscordSenderBase implements CommandSender {
msgtosend += "\n" + sendmsg; msgtosend += "\n" + sendmsg;
if (sendtask == null) if (sendtask == null)
sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
DiscordPlugin.sendMessageToChannel(channel, channel.createMessage((!broadcast && user != null ? user.getMention() + "\n" : "") + msgtosend.trim()).subscribe();
(!broadcast && user != null ? user.mention() + "\n" : "") + msgtosend.trim());
sendtask = null; sendtask = null;
msgtosend = ""; msgtosend = "";
}, 4); // Waits a 0.2 second to gather all/most of the different messages }, 4); // Waits a 0.2 second to gather all/most of the different messages

View file

@ -1,11 +0,0 @@
package buttondevteam.discordplugin;
import sx.blah.discord.handle.obj.IDiscordObject;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
import sx.blah.discord.util.RateLimitException;
@FunctionalInterface
public interface DiscordSupplier<T extends IDiscordObject<T>> {
public abstract T get() throws DiscordException, RateLimitException, MissingPermissionsException;
}

View file

@ -6,40 +6,48 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel;
import lombok.val; import lombok.val;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import sx.blah.discord.handle.obj.IChannel; import reactor.core.publisher.Flux;
import sx.blah.discord.handle.obj.IMessage; import reactor.core.publisher.Mono;
import java.io.File; import java.io.File;
import java.util.List;
public class AnnouncerModule extends Component<DiscordPlugin> { public class AnnouncerModule extends Component<DiscordPlugin> {
public ConfigData<IChannel> channel() { /**
* Channel to post new posts.
*/
public ReadOnlyConfigData<Mono<MessageChannel>> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); return DPUtils.channelData(getConfig(), "channel", 239519012529111040L);
} }
public ConfigData<IChannel> modChannel() { /**
* Channel where distinguished (moderator) posts go.
*/
public ReadOnlyConfigData<Mono<MessageChannel>> modChannel() {
return DPUtils.channelData(getConfig(), "modChannel", 239519012529111040L); return DPUtils.channelData(getConfig(), "modChannel", 239519012529111040L);
} }
/** /**
* Set to 0 or >50 to disable * Automatically unpins all messages except the last few. Set to 0 or >50 to disable
*/ */
public ConfigData<Short> keepPinned() { public ConfigData<Short> keepPinned() {
return getConfig().getData("keepPinned", (short) 40); return getConfig().getData("keepPinned", (short) 40);
} }
private ConfigData<Long> lastannouncementtime() { private ConfigData<Long> lastAnnouncementTime() {
return getConfig().getData("lastAnnouncementTime", 0L); return getConfig().getData("lastAnnouncementTime", 0L);
} }
private ConfigData<Long> lastseentime() { private ConfigData<Long> lastSeenTime() {
return getConfig().getData("lastSeenTime", 0L); return getConfig().getData("lastSeenTime", 0L);
} }
@ -50,24 +58,15 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
protected void enable() { protected void enable() {
if (DPUtils.disableIfConfigError(this, channel(), modChannel())) return; if (DPUtils.disableIfConfigError(this, channel(), modChannel())) return;
stop = false; //If not the first time stop = false; //If not the first time
DPUtils.performNoWait(() -> { val keepPinned = keepPinned().get();
try { if (keepPinned == 0) return;
val keepPinned = keepPinned().get(); Flux<Message> msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages);
if (keepPinned == 0) return; msgs.subscribe(Message::unpin);
val channel = channel().get();
List<IMessage> msgs = channel.getPinnedMessages();
for (int i = msgs.size() - 1; i >= keepPinned; i--) { // Unpin all pinned messages except the newest 10
channel.unpin(msgs.get(i));
Thread.sleep(10);
}
} catch (InterruptedException ignore) {
}
});
val yc = YamlConfiguration.loadConfiguration(new File("plugins/DiscordPlugin", "config.yml")); //Name change val yc = YamlConfiguration.loadConfiguration(new File("plugins/DiscordPlugin", "config.yml")); //Name change
if (lastannouncementtime().get() == 0) //Load old data if (lastAnnouncementTime().get() == 0) //Load old data
lastannouncementtime().set(yc.getLong("lastannouncementtime")); lastAnnouncementTime().set(yc.getLong("lastannouncementtime"));
if (lastseentime().get() == 0) if (lastSeenTime().get() == 0)
lastseentime().set(yc.getLong("lastseentime")); lastSeenTime().set(yc.getLong("lastseentime"));
new Thread(this::AnnouncementGetterThreadMethod).start(); new Thread(this::AnnouncementGetterThreadMethod).start();
} }
@ -88,7 +87,7 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
.get("children").getAsJsonArray(); .get("children").getAsJsonArray();
StringBuilder msgsb = new StringBuilder(); StringBuilder msgsb = new StringBuilder();
StringBuilder modmsgsb = new StringBuilder(); StringBuilder modmsgsb = new StringBuilder();
long lastanntime = lastannouncementtime().get(); long lastanntime = lastAnnouncementTime().get();
for (int i = json.size() - 1; i >= 0; i--) { for (int i = json.size() - 1; i >= 0; i--) {
JsonObject item = json.get(i).getAsJsonObject(); JsonObject item = json.get(i).getAsJsonObject();
final JsonObject data = item.get("data").getAsJsonObject(); final JsonObject data = item.get("data").getAsJsonObject();
@ -101,9 +100,9 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
distinguished = distinguishedjson.getAsString(); distinguished = distinguishedjson.getAsString();
String permalink = "https://www.reddit.com" + data.get("permalink").getAsString(); String permalink = "https://www.reddit.com" + data.get("permalink").getAsString();
long date = data.get("created_utc").getAsLong(); long date = data.get("created_utc").getAsLong();
if (date > lastseentime().get()) if (date > lastSeenTime().get())
lastseentime().set(date); lastSeenTime().set(date);
else if (date > lastannouncementtime().get()) { else if (date > lastAnnouncementTime().get()) {
do { do {
val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit"); val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit");
if (reddituserclass == null) if (reddituserclass == null)
@ -122,13 +121,13 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
} }
} }
if (msgsb.length() > 0) if (msgsb.length() > 0)
channel().get().pin(DiscordPlugin.sendMessageToChannelWait(channel().get(), msgsb.toString())); channel().get().flatMap(ch -> ch.createMessage(msgsb.toString()))
.flatMap(Message::pin).subscribe();
if (modmsgsb.length() > 0) if (modmsgsb.length() > 0)
DiscordPlugin.sendMessageToChannel(modChannel().get(), modmsgsb.toString()); modChannel().get().flatMap(ch -> ch.createMessage(modmsgsb.toString()))
if (lastannouncementtime().get() != lastanntime) { .flatMap(Message::pin).subscribe();
lastannouncementtime().set(lastanntime); // If sending succeeded if (lastAnnouncementTime().get() != lastanntime)
getPlugin().saveConfig(); //TODO: Won't be needed if I implement auto-saving lastAnnouncementTime().set(lastanntime); // If sending succeeded
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }

View file

@ -3,6 +3,8 @@ package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import java.lang.reflect.Method;
public class Command2DC extends Command2<ICommand2DC, Command2DCSender> { public class Command2DC extends Command2<ICommand2DC, Command2DCSender> {
@Override @Override
public void registerCommand(ICommand2DC command) { public void registerCommand(ICommand2DC command) {
@ -10,8 +12,8 @@ public class Command2DC extends Command2<ICommand2DC, Command2DCSender> {
} }
@Override @Override
public boolean hasPermission(Command2DCSender sender, ICommand2DC command) { 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 !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way?
return true; return true;
} }
} }

View file

@ -2,20 +2,28 @@ package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DPUtils;
import buttondevteam.lib.chat.Command2Sender; import buttondevteam.lib.chat.Command2Sender;
import discord4j.core.object.entity.Message;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import sx.blah.discord.handle.obj.IMessage; import lombok.val;
@RequiredArgsConstructor @RequiredArgsConstructor
public class Command2DCSender implements Command2Sender { public class Command2DCSender implements Command2Sender {
private final @Getter IMessage message; private final @Getter
Message message;
@Override @Override
public void sendMessage(String message) { public void sendMessage(String message) {
if (message.length() == 0) return; if (message.length() == 0) return;
message = DPUtils.sanitizeString(message); message = DPUtils.sanitizeString(message);
message = Character.toLowerCase(message.charAt(0)) + message.substring(1); message = Character.toLowerCase(message.charAt(0)) + message.substring(1);
this.message.reply(message); 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 @Override

View file

@ -1,7 +1,6 @@
package buttondevteam.discordplugin.commands; package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
@ -28,34 +27,37 @@ public class ConnectCommand extends ICommand2DC {
@Command2.Subcommand @Command2.Subcommand
public boolean def(Command2DCSender sender, String Minecraftname) { public boolean def(Command2DCSender sender, String Minecraftname) {
val message = sender.getMessage(); val message = sender.getMessage();
if (WaitingToConnect.inverse().containsKey(message.getAuthor().getStringID())) { val channel = message.getChannel().block();
DiscordPlugin.sendMessageToChannel(message.getChannel(), val author = message.getAuthor().orElse(null);
"Replacing " + WaitingToConnect.inverse().get(message.getAuthor().getStringID()) + " with " + Minecraftname); if (author == null || channel == null) return true;
WaitingToConnect.inverse().remove(message.getAuthor().getStringID()); 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") @SuppressWarnings("deprecation")
OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname); OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname);
if (p == null) { if (p == null) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "The specified Minecraft player cannot be found"); channel.createMessage("The specified Minecraft player cannot be found").subscribe();
return true; return true;
} }
try (TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class)) { try (TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class)) {
DiscordPlayer dp = pl.getAs(DiscordPlayer.class); DiscordPlayer dp = pl.getAs(DiscordPlayer.class);
if (dp != null && message.getAuthor().getStringID().equals(dp.getDiscordID())) { if (dp != null && author.getId().asString().equals(dp.getDiscordID())) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "You already have this account connected."); channel.createMessage("You already have this account connected.").subscribe();
return true; return true;
} }
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e); TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e);
DiscordPlugin.sendMessageToChannel(message.getChannel(), "An internal error occured!\n" + e); channel.createMessage("An internal error occured!\n" + e).subscribe();
} }
WaitingToConnect.put(p.getName(), message.getAuthor().getStringID()); WaitingToConnect.put(p.getName(), author.getId().asString());
DiscordPlugin.sendMessageToChannel(message.getChannel(), channel.createMessage(
"Alright! Now accept the connection in Minecraft from the account " + Minecraftname "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 with the same command."); + " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command.").subscribe();
if (p.isOnline()) if (p.isOnline())
((Player) p).sendMessage("§bTo connect with the Discord account " + message.getAuthor().getName() + "#" ((Player) p).sendMessage("§bTo connect with the Discord account " + author.getUsername() + "#"
+ message.getAuthor().getDiscriminator() + " do /discord accept"); + author.getDiscriminator() + " do /discord accept");
return true; return true;
} }

View file

@ -4,17 +4,27 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.listeners.CommonListeners; import buttondevteam.discordplugin.listeners.CommonListeners;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import reactor.core.publisher.Mono;
@CommandClass(helpText = { @CommandClass(helpText = {
"Switches debug mode." "Switches debug mode."
}) })
public class DebugCommand extends ICommand2DC { public class DebugCommand extends ICommand2DC {
@Command2.Subcommand @Command2.Subcommand
public boolean def(Command2DCSender sender, String args) { public boolean def(Command2DCSender sender) {
if (sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.ModRole().get())) sender.getMessage().getAuthorAsMember()
sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled")); .switchIfEmpty(sender.getMessage().getAuthor() //Support DMs
else .map(u -> u.asMember(DiscordPlugin.mainServer.getId()))
sender.sendMessage("you need to be a moderator to use this command."); .orElse(Mono.empty()))
return true; .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
.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,5 +1,6 @@
package buttondevteam.discordplugin.commands; package buttondevteam.discordplugin.commands;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
@CommandClass(helpText = { @CommandClass(helpText = {
@ -7,12 +8,17 @@ import buttondevteam.lib.chat.CommandClass;
"Shows some info about a command or lists the available commands.", // "Shows some info about a command or lists the available commands.", //
}) })
public class HelpCommand extends ICommand2DC { public class HelpCommand extends ICommand2DC {
@Override @Command2.Subcommand
public boolean def(Command2DCSender sender, String args) { public boolean def(Command2DCSender sender, @Command2.TextArg @Command2.OptionalArg String args) {
if (args.length() == 0) if (args == null || args.length() == 0)
sender.sendMessage(getManager().getCommandsText()); sender.sendMessage(getManager().getCommandsText());
else else {
sender.sendMessage("Soon:tm:"); //TODO String[] ht = getManager().getHelpText(args);
return true; if (ht == null)
sender.sendMessage("Command not found: " + args);
else
sender.sendMessage(ht);
}
return true;
} }
} }

View file

@ -7,13 +7,11 @@ import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.ChromaGamerBase.InfoTarget; import buttondevteam.lib.player.ChromaGamerBase.InfoTarget;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.User;
import lombok.val; import lombok.val;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@CommandClass(helpText = { @CommandClass(helpText = {
"User information", // "User information", //
@ -24,67 +22,71 @@ public class UserinfoCommand extends ICommand2DC {
@Command2.Subcommand @Command2.Subcommand
public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) { public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) {
val message = sender.getMessage(); val message = sender.getMessage();
IUser target = null; User target = null;
val channel = message.getChannel().block();
assert channel != null;
if (user == null || user.length() == 0) if (user == null || user.length() == 0)
target = message.getAuthor(); target = message.getAuthor().orElse(null);
else { else {
final Optional<IUser> firstmention = message.getMentions().stream() @SuppressWarnings("OptionalGetWithoutIsPresent") final User firstmention = message.getUserMentions()
.filter(m -> !m.getStringID().equals(DiscordPlugin.dc.getOurUser().getStringID())).findFirst(); .filter(m -> !m.getId().asString().equals(DiscordPlugin.dc.getSelfId().get().asString())).blockFirst();
if (firstmention.isPresent()) if (firstmention != null)
target = firstmention.get(); target = firstmention;
else if (user.contains("#")) { else if (user.contains("#")) {
String[] targettag = user.split("#"); String[] targettag = user.split("#");
final List<IUser> targets = getUsers(message, targettag[0]); final List<User> targets = getUsers(message, targettag[0]);
if (targets.size() == 0) { if (targets.size() == 0) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), channel.createMessage("The user cannot be found (by name): " + user).subscribe();
"The user cannot be found (by name): " + user); return true;
return true;
} }
for (IUser ptarget : targets) { for (User ptarget : targets) {
if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) { if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) {
target = ptarget; target = ptarget;
break; break;
} }
} }
if (target == null) { if (target == null) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size()
"The user cannot be found (by discriminator): " + user + "(Found " + targets.size() + " users with the name.)").subscribe();
+ " users with the name.)"); return true;
return true;
} }
} else { } else {
final List<IUser> targets = getUsers(message, user); final List<User> targets = getUsers(message, user);
if (targets.size() == 0) { if (targets.size() == 0) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), channel.createMessage("The user cannot be found on Discord: " + user).subscribe();
"The user cannot be found on Discord: " + user); return true;
return true;
} }
if (targets.size() > 1) { if (targets.size() > 1) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe();
"Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping."); return true;
return true;
} }
target = targets.get(0); target = targets.get(0);
} }
} }
try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getStringID(), DiscordPlayer.class)) { if (target == null) {
StringBuilder uinfo = new StringBuilder("User info for ").append(target.getName()).append(":\n"); sender.sendMessage("An error occurred.");
uinfo.append(dp.getInfo(InfoTarget.Discord)); return true;
DiscordPlugin.sendMessageToChannel(message.getChannel(), uinfo.toString());
} catch (Exception e) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while getting the user!");
TBMCCoreAPI.SendException("Error while getting info about " + target.getName() + "!", e);
} }
return true; try (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();
} catch (Exception e) {
channel.createMessage("An error occured while getting the user!").subscribe();
TBMCCoreAPI.SendException("Error while getting info about " + target.getUsername() + "!", e);
}
return true;
} }
private List<IUser> getUsers(IMessage message, String args) { private List<User> getUsers(Message message, String args) {
final List<IUser> targets; final List<User> targets;
if (message.getChannel().isPrivate()) val guild = message.getGuild().block();
targets = DiscordPlugin.dc.getUsers().stream().filter(u -> u.getName().equalsIgnoreCase(args)) if (guild == null) //Private channel
.collect(Collectors.toList()); targets = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equalsIgnoreCase(args))
.collectList().block();
else else
targets = message.getGuild().getUsersByName(args, true); targets = guild.getMembers().filter(m -> m.getUsername().equalsIgnoreCase(args))
.map(m -> (User) m).collectList().block();
return targets; return targets;
} }

View file

@ -3,10 +3,12 @@ package buttondevteam.discordplugin.exceptions;
import buttondevteam.core.ComponentManager; import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCDebugMessageEvent; import buttondevteam.lib.TBMCDebugMessageEvent;
import discord4j.core.object.entity.MessageChannel;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import reactor.core.publisher.Mono;
public class DebugMessageListener implements Listener{ public class DebugMessageListener implements Listener {
@EventHandler @EventHandler
public void onDebugMessage(TBMCDebugMessageEvent e) { public void onDebugMessage(TBMCDebugMessageEvent e) {
SendMessage(e.getDebugMessage()); SendMessage(e.getDebugMessage());
@ -17,13 +19,15 @@ public class DebugMessageListener implements Listener{
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class)) if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class))
return; return;
try { try {
Mono<MessageChannel> mc = ExceptionListenerModule.getChannel();
if (mc == null) return;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("```").append("\n"); sb.append("```").append("\n");
if (message.length() > 2000) if (message.length() > 2000)
message = message.substring(0, 2000); message = message.substring(0, 2000);
sb.append(message).append("\n"); sb.append(message).append("\n");
sb.append("```"); sb.append("```");
DiscordPlugin.sendMessageToChannel(ExceptionListenerModule.getChannel(), sb.toString()); mc.flatMap(ch -> ch.createMessage(sb.toString())).subscribe();
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }

View file

@ -7,13 +7,16 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCExceptionEvent; import buttondevteam.lib.TBMCExceptionEvent;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.GuildChannel;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.Role;
import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang.exception.ExceptionUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import sx.blah.discord.handle.obj.IChannel; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IRole;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -21,64 +24,71 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener { public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener {
private List<Throwable> lastthrown = new ArrayList<>(); private List<Throwable> lastthrown = new ArrayList<>();
private List<String> lastsourcemsg = new ArrayList<>(); private List<String> lastsourcemsg = new ArrayList<>();
@EventHandler @EventHandler
public void onException(TBMCExceptionEvent e) { public void onException(TBMCExceptionEvent e) {
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass())) if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass()))
return; return;
if (lastthrown.stream() if (lastthrown.stream()
.anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace()) .anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace())
&& (e.getException().getMessage() == null ? ex.getMessage() == null && (e.getException().getMessage() == null ? ex.getMessage() == null
: e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message : e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message
&& lastsourcemsg.contains(e.getSourceMessage())) && lastsourcemsg.contains(e.getSourceMessage()))
return; return;
SendException(e.getException(), e.getSourceMessage()); SendException(e.getException(), e.getSourceMessage());
if (lastthrown.size() >= 10) if (lastthrown.size() >= 10)
lastthrown.remove(0); lastthrown.remove(0);
if (lastsourcemsg.size() >= 10) if (lastsourcemsg.size() >= 10)
lastsourcemsg.remove(0); lastsourcemsg.remove(0);
lastthrown.add(e.getException()); lastthrown.add(e.getException());
lastsourcemsg.add(e.getSourceMessage()); lastsourcemsg.add(e.getSourceMessage());
e.setHandled(); e.setHandled();
} }
private static void SendException(Throwable e, String sourcemessage) { private static void SendException(Throwable e, String sourcemessage) {
if (instance == null) return; if (instance == null) return;
try { try {
IChannel channel = getChannel(); Mono<MessageChannel> channel = getChannel();
assert channel != null; assert channel != null;
IRole coderRole = instance.pingRole(channel.getGuild()).get(); Mono<Role> coderRole;
StringBuilder sb = TBMCCoreAPI.IsTestServer() ? new StringBuilder() if (channel instanceof GuildChannel)
: new StringBuilder(coderRole == null ? "" : coderRole.mention()).append("\n"); coderRole = instance.pingRole(((GuildChannel) channel).getGuild()).get();
sb.append(sourcemessage).append("\n"); else
sb.append("```").append("\n"); coderRole = Mono.empty();
String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) coderRole.map(role -> TBMCCoreAPI.IsTestServer() ? new StringBuilder()
.filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam.")) : new StringBuilder(role.getMention()).append("\n"))
.collect(Collectors.joining("\n")); .defaultIfEmpty(new StringBuilder())
if (stackTrace.length() > 1800) .flatMap(sb -> {
stackTrace = stackTrace.substring(0, 1800); sb.append(sourcemessage).append("\n");
sb.append(stackTrace).append("\n"); sb.append("```").append("\n");
sb.append("```"); String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n"))
DiscordPlugin.sendMessageToChannel(channel, sb.toString()); //Instance isn't null here .filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam."))
} catch (Exception ex) { .collect(Collectors.joining("\n"));
ex.printStackTrace(); if (sb.length() + stackTrace.length() >= 1980)
} stackTrace = stackTrace.substring(0, 1980 - sb.length());
} sb.append(stackTrace).append("\n");
sb.append("```");
return channel.flatMap(ch -> ch.createMessage(sb.toString()));
}).subscribe();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static ExceptionListenerModule instance; private static ExceptionListenerModule instance;
public static IChannel getChannel() { public static Mono<MessageChannel> getChannel() {
if (instance != null) return instance.channel().get(); if (instance != null) return instance.channel().get();
return null; return Mono.empty();
} }
private ConfigData<IChannel> channel() { private ReadOnlyConfigData<Mono<MessageChannel>> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); return DPUtils.channelData(getConfig(), "channel", 239519012529111040L);
} }
private ConfigData<IRole> pingRole(IGuild guild) { private ConfigData<Mono<Role>> pingRole(Mono<Guild> guild) {
return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild); return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild);
} }

View file

@ -6,15 +6,17 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import discord4j.core.event.domain.PresenceUpdateEvent;
import discord4j.core.object.entity.*;
import discord4j.core.object.presence.Status;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.*;
import sx.blah.discord.util.EmbedBuilder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -23,36 +25,43 @@ import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream; import java.util.stream.IntStream;
/**
* The YEEHAW event uses an emoji named :YEEHAW: if available
*/
public class FunModule extends Component<DiscordPlugin> implements Listener { public class FunModule extends Component<DiscordPlugin> implements Listener {
private static final String[] serverReadyStrings = new String[]{"In one week from now", // Ali private static final String[] serverReadyStrings = new String[]{"In one week from now", // Ali
"Between now and the heat-death of the universe.", // Ghostise "Between now and the heat-death of the universe.", // Ghostise
"Soon™", "Ask again this time next month", // Ghostise "Soon™", "Ask again this time next month", // Ghostise
"In about 3 seconds", // Nicolai "In about 3 seconds", // Nicolai
"After we finish 8 plugins", // Ali "After we finish 8 plugins", // Ali
"Tomorrow.", // Ali "Tomorrow.", // Ali
"After one tiiiny feature", // Ali "After one tiiiny feature", // Ali
"Next commit", // Ali "Next commit", // Ali
"After we finish strangling Towny", // Ali "After we finish strangling Towny", // Ali
"When we kill every *fucking* bug", // Ali "When we kill every *fucking* bug", // Ali
"Once the server stops screaming.", // Ali "Once the server stops screaming.", // Ali
"After HL3 comes out", // Ali "After HL3 comes out", // Ali
"Next time you ask", // Ali "Next time you ask", // Ali
"When will *you* be open?" // Ali "When will *you* be open?" // Ali
}; };
private ConfigData<Boolean> serverReady() { /**
return getConfig().getData("serverReady", true); * Questions that the bot will choose a random answer to give to.
*/
private ConfigData<String[]> serverReady() {
return 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 ConfigData<ArrayList<String>> serverReadyAnswers() { private ConfigData<ArrayList<String>> serverReadyAnswers() {
return getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); //TODO: Test return getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); //TODO: Test
} }
private static final String[] serverReadyQuestions = 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?"};
private static final Random serverReadyRandom = new Random(); private static final Random serverReadyRandom = new Random();
private static final ArrayList<Short> usableServerReadyStrings = new ArrayList<>(0); private static final ArrayList<Short> usableServerReadyStrings = new ArrayList<>(0);
@ -76,10 +85,10 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
private static short ListC = 0; private static short ListC = 0;
public static boolean executeMemes(IMessage message) { public static boolean executeMemes(Message message) {
val fm = ComponentManager.getIfEnabled(FunModule.class); val fm = ComponentManager.getIfEnabled(FunModule.class);
if (fm == null) return false; if (fm == null) return false;
String msglowercased = message.getContent().toLowerCase(); String msglowercased = message.getContent().orElse("").toLowerCase();
lastlist++; lastlist++;
if (lastlist > 5) { if (lastlist > 5) {
ListC = 0; ListC = 0;
@ -87,22 +96,20 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
} }
if (msglowercased.equals("list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already if (msglowercased.equals("list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already
{ {
message.reply("Stop it. You know the answer."); DPUtils.reply(message, null, "Stop it. You know the answer.").subscribe();
lastlist = 0; lastlist = 0;
lastlistp = (short) Bukkit.getOnlinePlayers().size(); lastlistp = (short) Bukkit.getOnlinePlayers().size();
return true; //Handled return true; //Handled
} }
lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle
if (fm.serverReady().get()) { if (!TBMCCoreAPI.IsTestServer()
if (!TBMCCoreAPI.IsTestServer() && Arrays.stream(fm.serverReady().get()).anyMatch(msglowercased::contains)) {
&& Arrays.stream(serverReadyQuestions).anyMatch(msglowercased::contains)) { int next;
int next; if (usableServerReadyStrings.size() == 0)
if (usableServerReadyStrings.size() == 0) fm.createUsableServerReadyStrings();
fm.createUsableServerReadyStrings(); next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size()));
next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size())); DPUtils.reply(message, null, fm.serverReadyAnswers().get().get(next)).subscribe();
DiscordPlugin.sendMessageToChannel(message.getChannel(), serverReadyStrings[next]); return false; //Still process it as a command/mcchat if needed
return false; //Still process it as a command/mcchat if needed
}
} }
return false; return false;
} }
@ -112,12 +119,12 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
ListC = 0; ListC = 0;
} }
private ConfigData<IRole> fullHouseDevRole(IGuild guild) { private ConfigData<Mono<Role>> fullHouseDevRole(Mono<Guild> guild) {
return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild); return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild);
} }
private ConfigData<IChannel> fullHouseChannel() { private ReadOnlyConfigData<Mono<MessageChannel>> fullHouseChannel() {
return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L); return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L);
} }
@ -126,24 +133,24 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
public static void handleFullHouse(PresenceUpdateEvent event) { public static void handleFullHouse(PresenceUpdateEvent event) {
val fm = ComponentManager.getIfEnabled(FunModule.class); val fm = ComponentManager.getIfEnabled(FunModule.class);
if (fm == null) return; if (fm == null) return;
val channel = fm.fullHouseChannel().get(); if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 != 0) return;
if (channel == null) return; fm.fullHouseChannel().get()
val devrole = fm.fullHouseDevRole(channel.getGuild()).get(); .filter(ch -> ch instanceof GuildChannel)
if (devrole == null) return; .flatMap(channel -> fm.fullHouseDevRole(((GuildChannel) channel).getGuild()).get()
if (event.getOldPresence().getStatus().equals(StatusType.OFFLINE) .filter(role -> event.getOld().map(p -> p.getStatus().equals(Status.OFFLINE)).orElse(false))
&& !event.getNewPresence().getStatus().equals(StatusType.OFFLINE) .filter(role -> !event.getCurrent().getStatus().equals(Status.OFFLINE))
&& event.getUser().getRolesForGuild(channel.getGuild()).stream() .filterWhen(devrole -> event.getMember().flatMap(m -> m.getRoles()
.anyMatch(r -> r.getLongID() == devrole.getLongID()) .any(r -> r.getId().asLong() == devrole.getId().asLong())))
&& channel.getGuild().getUsersByRole(devrole).stream() .filterWhen(devrole ->
.noneMatch(u -> u.getPresence().getStatus().equals(StatusType.OFFLINE)) event.getGuild().flatMapMany(g -> g.getMembers().filter(m -> m.getRoleIds().stream().anyMatch(s -> s.equals(devrole.getId()))))
&& lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime()) .flatMap(Member::getPresence).all(pr -> !pr.getStatus().equals(Status.OFFLINE)))
&& Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 == 0) { .filter(devrole -> lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime())) //This should stay so it checks this last
DiscordPlugin.sendMessageToChannel(channel, "Full house!", .flatMap(devrole -> {
new EmbedBuilder() lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime());
.withImage( return channel.createMessage(mcs -> mcs.setContent("Full house!").setEmbed(ecs ->
"https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png") ecs.setImage(
.build()); "https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png")
lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime()); ));
} })).subscribe();
} }
} }

View file

@ -1,11 +1,18 @@
package buttondevteam.discordplugin.listeners; package buttondevteam.discordplugin.listeners;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender; import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.discordplugin.util.Timings;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import sx.blah.discord.handle.obj.IChannel; import discord4j.core.object.entity.Message;
import sx.blah.discord.handle.obj.IMessage; import discord4j.core.object.entity.MessageChannel;
import sx.blah.discord.handle.obj.IRole; import discord4j.core.object.entity.PrivateChannel;
import discord4j.core.object.entity.Role;
import lombok.val;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicBoolean;
public class CommandListener { public class CommandListener {
/** /**
@ -13,44 +20,61 @@ public class CommandListener {
* *
* @param message The Discord message * @param message The Discord message
* @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message
* @return Whether it ran the command * @return Whether it <b>did not run</b> the command
*/ */
public static boolean runCommand(IMessage message, boolean mentionedonly) { public static Mono<Boolean> runCommand(Message message, MessageChannel commandChannel, boolean mentionedonly) {
if (message.getContent().length() == 0) Timings timings = CommonListeners.timings;
return false; //Pin messages and such, let the mcchat listener deal with it Mono<Boolean> ret = Mono.just(true);
final IChannel channel = message.getChannel(); if (!message.getContent().isPresent())
if (!mentionedonly) { //mentionedonly conditions are in CommonListeners return ret; //Pin messages and such, let the mcchat listener deal with it
if (!message.getChannel().isPrivate() val content = message.getContent().get();
&& !(message.getContent().charAt(0) == DiscordPlugin.getPrefix() timings.printElapsed("A");
&& channel.getStringID().equals(DiscordPlugin.plugin.CommandChannel().get().getStringID()))) // return message.getChannel().flatMap(channel -> {
return false; Mono<?> tmp = ret;
message.getChannel().setTypingStatus(true); // Fun if (!mentionedonly) { //mentionedonly conditions are in CommonListeners
} timings.printElapsed("B");
final StringBuilder cmdwithargs = new StringBuilder(message.getContent()); if (!(channel instanceof PrivateChannel)
final String mention = DiscordPlugin.dc.getOurUser().mention(false); && !(content.charAt(0) == DiscordPlugin.getPrefix()
final String mentionNick = DiscordPlugin.dc.getOurUser().mention(true); && channel.getId().asLong() == commandChannel.getId().asLong())) //
boolean gotmention = checkanddeletemention(cmdwithargs, mention, message); return ret;
gotmention = checkanddeletemention(cmdwithargs, mentionNick, message) || gotmention; timings.printElapsed("C");
for (String mentionRole : (Iterable<String>) message.getRoleMentions().stream().filter(r -> DiscordPlugin.dc.getOurUser().hasRole(r)).map(IRole::mention)::iterator) tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x)
gotmention = checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention; // Delete all mentions }
if (mentionedonly && !gotmention) { final StringBuilder cmdwithargs = new StringBuilder(content);
message.getChannel().setTypingStatus(false); val gotmention = new AtomicBoolean();
return false; timings.printElapsed("Before self");
} return tmp.flatMapMany(x ->
message.getChannel().setTypingStatus(true); DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId()))
String cmdwithargsString = cmdwithargs.toString(); .flatMapMany(self -> {
try { timings.printElapsed("D");
if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message));
message.reply("Unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString); gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get());
} catch (Exception e) { val mentions = message.getRoleMentions();
TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e); return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName())))
} .map(Role::getMention);
message.getChannel().setTypingStatus(false); }).map(mentionRole -> {
return true; 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.\n" + cmdwithargsString)
.map(m -> false);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e);
}
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, IMessage message) { private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) {
if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text final char prefix = DiscordPlugin.getPrefix();
if (message.getContent().orElse("").startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text
if (cmdwithargs.length() > mention.length() + 1) { if (cmdwithargs.length() > mention.length() + 1) {
int i = cmdwithargs.indexOf(" ", mention.length()); int i = cmdwithargs.indexOf(" ", mention.length());
if (i == -1) if (i == -1)
@ -60,14 +84,16 @@ public class CommandListener {
for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++) for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++)
; //Removes any space before the command ; //Removes any space before the command
cmdwithargs.delete(0, i); cmdwithargs.delete(0, i);
cmdwithargs.insert(0, DiscordPlugin.getPrefix()); //Always use the prefix for processing cmdwithargs.insert(0, prefix); //Always use the prefix for processing
} else } else
cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help"); cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help");
else { else {
if (cmdwithargs.length() == 0)
cmdwithargs.replace(0, cmdwithargs.length(), 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 false; //Don't treat / as mention, mentions can be used in public mcchat
} }
if (cmdwithargs.length() == 0)
cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help");
return true; return true;
} }
} }

View file

@ -5,18 +5,23 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.fun.FunModule; import buttondevteam.discordplugin.fun.FunModule;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.role.GameRoleModule; import buttondevteam.discordplugin.role.GameRoleModule;
import buttondevteam.discordplugin.util.Timings;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component; 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.PrivateChannel;
import lombok.val; import lombok.val;
import sx.blah.discord.api.events.IListener; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent;
import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent;
public class CommonListeners { public class CommonListeners {
public static final Timings timings = new Timings();
/* /*
MentionEvent: MentionEvent:
- CommandListener (starts with mention, only 'channelcon' and not in #bot) - CommandListener (starts with mention, only 'channelcon' and not in #bot)
@ -26,52 +31,59 @@ public class CommonListeners {
- Minecraft chat (is enabled in the channel and message isn't [/]mcchat) - Minecraft chat (is enabled in the channel and message isn't [/]mcchat)
- CommandListener (with the correct prefix in #bot, or in private) - CommandListener (with the correct prefix in #bot, or in private)
*/ */
public static IListener<?>[] getListeners() { public static void register(EventDispatcher dispatcher) {
return new IListener[]{new IListener<MessageReceivedEvent>() { dispatcher.on(MessageCreateEvent.class).flatMap(event -> {
@Override timings.printElapsed("Message received");
public void handle(MessageReceivedEvent event) { val def = Mono.empty();
if (DiscordPlugin.SafeMode) if (DiscordPlugin.SafeMode)
return; return def;
if (event.getMessage().getAuthor().isBot()) val author = event.getMessage().getAuthor();
return; if (!author.isPresent() || author.get().isBot())
if (FunModule.executeMemes(event.getMessage())) return def;
return; if (FunModule.executeMemes(event.getMessage()))
try { return def;
boolean handled = false; val commandChannel = DiscordPlugin.plugin.commandChannel().get();
val commandChannel = DiscordPlugin.plugin.CommandChannel().get(); val commandCh = DPUtils.getMessageChannel(DiscordPlugin.plugin.commandChannel());
if ((commandChannel != null && event.getChannel().getLongID() == commandChannel.getLongID()) //If mentioned, that's higher than chat return commandCh.filterWhen(ch -> event.getMessage().getChannel().map(mch ->
|| event.getMessage().getContent().contains("channelcon")) //Only 'channelcon' is allowed in other channels (commandChannel != null && mch.getId().asLong() == commandChannel.asLong()) //If mentioned, that's higher than chat
handled = CommandListener.runCommand(event.getMessage(), true); //#bot is handled here || mch instanceof PrivateChannel
if (handled) return; || event.getMessage().getContent().orElse("").contains("channelcon")) //Only 'channelcon' is allowed in other channels
val mcchat = Component.getComponents().get(MinecraftChatModule.class); .flatMap(shouldRun -> { //Only continue if this doesn't handle the event
if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again if (!shouldRun)
handled = ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels return Mono.just(true); //The condition is only for the first command execution, not mcchat
if (!handled) timings.printElapsed("Run command 1");
handled = CommandListener.runCommand(event.getMessage(), false); return CommandListener.runCommand(event.getMessage(), ch, true); //#bot is handled here
} catch (Exception e) { })).filterWhen(ch -> {
TBMCCoreAPI.SendException("An error occured while handling a message!", e); timings.printElapsed("mcchat");
} val mcchat = Component.getComponents().get(MinecraftChatModule.class);
} if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again
}, new IListener<sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent>() { return ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels
@Override return Mono.empty(); //Wasn't handled, continue
public void handle(PresenceUpdateEvent event) { }).filterWhen(ch -> {
if (DiscordPlugin.SafeMode) timings.printElapsed("Run command 2");
return; return CommandListener.runCommand(event.getMessage(), ch, false);
FunModule.handleFullHouse(event); });
} }).onErrorContinue((err, obj) -> TBMCCoreAPI.SendException("An error occured while handling a message!", err))
}, (IListener<RoleCreateEvent>) GameRoleModule::handleRoleEvent, // .subscribe();
(IListener<RoleDeleteEvent>) GameRoleModule::handleRoleEvent, // dispatcher.on(PresenceUpdateEvent.class).subscribe(event -> {
(IListener<RoleUpdateEvent>) GameRoleModule::handleRoleEvent}; 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; private static boolean debug = false;
public static void debug(String debug) { public static void debug(String debug) {
if (CommonListeners.debug) //Debug if (CommonListeners.debug) //Debug
DPUtils.getLogger().info(debug); DPUtils.getLogger().info(debug);
} }
public static boolean debug() { public static boolean debug() {
return debug = !debug; return debug = !debug;
} }
} }

View file

@ -5,39 +5,50 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.ConnectCommand; import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.lib.player.TBMCPlayerGetInfoEvent; import buttondevteam.lib.player.TBMCPlayerGetInfoEvent;
import buttondevteam.lib.player.TBMCPlayerJoinEvent; import buttondevteam.lib.player.TBMCPlayerJoinEvent;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.User;
import discord4j.core.object.util.Snowflake;
import lombok.val;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.server.ServerCommandEvent; import org.bukkit.event.server.ServerCommandEvent;
import sx.blah.discord.handle.obj.IUser;
public class MCListener implements Listener { public class MCListener implements Listener {
@EventHandler @EventHandler
public void onPlayerJoin(TBMCPlayerJoinEvent e) { public void onPlayerJoin(TBMCPlayerJoinEvent e) {
if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) { if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) {
@SuppressWarnings("ConstantConditions") IUser user = DiscordPlugin.dc @SuppressWarnings("ConstantConditions") User user = DiscordPlugin.dc
.getUserByID(Long.parseLong(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))); .getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))).block();
e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getName() + "#" + user.getDiscriminator() if (user == null) return;
e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getUsername() + "#" + user.getDiscriminator()
+ " do /discord accept"); + " do /discord accept");
e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline"); e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline");
} }
} }
@EventHandler @EventHandler
public void onGetInfo(TBMCPlayerGetInfoEvent e) { public void onGetInfo(TBMCPlayerGetInfoEvent e) {
if (DiscordPlugin.SafeMode) if (DiscordPlugin.SafeMode)
return; return;
DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class);
if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals(""))
return; return;
IUser user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); User user = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).block();
e.addInfo("Discord tag: " + user.getName() + "#" + user.getDiscriminator()); if (user == null) return;
e.addInfo(user.getPresence().getStatus().toString()); e.addInfo("Discord tag: " + user.getUsername() + "#" + user.getDiscriminator());
if (user.getPresence().getActivity().isPresent() && user.getPresence().getText().isPresent()) Member member = user.asMember(DiscordPlugin.mainServer.getId()).block();
e.addInfo(user.getPresence().getActivity().get() + ": " + user.getPresence().getText().get()); if (member == null) return;
} val pr = member.getPresence().block();
if (pr == null) return;
e.addInfo(pr.getStatus().toString());
if (pr.getActivity().isPresent()) {
val activity = pr.getActivity().get();
e.addInfo(activity.getType() + ": " + activity.getName());
}
}
@EventHandler @EventHandler
public void onServerCommand(ServerCommandEvent e) { public void onServerCommand(ServerCommandEvent e) {
DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped
} }
} }

View file

@ -9,15 +9,16 @@ import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.TBMCPlayer; import buttondevteam.lib.player.TBMCPlayer;
import discord4j.core.object.entity.Message;
import discord4j.core.object.util.Permission;
import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.Permissions;
import sx.blah.discord.util.PermissionUtils;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,15 +33,17 @@ import java.util.stream.Collectors;
"Mentioning the bot is needed in this case because the / prefix only works in #bot.", // "Mentioning the bot is needed in this case because the / prefix only works in #bot.", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=226443037893591041&scope=bot&permissions=268509264>" // "Invite link: <https://discordapp.com/oauth2/authorize?client_id=226443037893591041&scope=bot&permissions=268509264>" //
}) })
@RequiredArgsConstructor
public class ChannelconCommand extends ICommand2DC { public class ChannelconCommand extends ICommand2DC {
private final MinecraftChatModule module;
@Command2.Subcommand @Command2.Subcommand
public boolean remove(Command2DCSender sender) { public boolean remove(Command2DCSender sender) {
val message = sender.getMessage(); val message = sender.getMessage();
if (checkPerms(message)) return true; if (checkPerms(message)) return true;
if (MCChatCustom.removeCustomChat(message.getChannel())) if (MCChatCustom.removeCustomChat(message.getChannelId()))
message.reply("channel connection removed."); DPUtils.reply(message, null, "channel connection removed.").subscribe();
else else
message.reply("this channel isn't connected."); DPUtils.reply(message, null, "this channel isn't connected.").subscribe();
return true; return true;
} }
@ -48,13 +51,13 @@ public class ChannelconCommand extends ICommand2DC {
public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) { public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) {
val message = sender.getMessage(); val message = sender.getMessage();
if (checkPerms(message)) return true; if (checkPerms(message)) return true;
val cc = MCChatCustom.getCustomChat(message.getChannel()); val cc = MCChatCustom.getCustomChat(message.getChannelId());
if (cc == null) if (cc == null)
return respond(sender, "this channel isn't connected."); 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")) 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")); + "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n"));
if (toggle == null) { if (toggle == null) {
message.reply("toggles:\n" + togglesString.get()); DPUtils.reply(message, null, "toggles:\n" + togglesString.get()).subscribe();
return true; return true;
} }
String arg = toggle.toUpperCase(); String arg = toggle.toUpperCase();
@ -62,7 +65,7 @@ public class ChannelconCommand extends ICommand2DC {
if (!b.isPresent()) { if (!b.isPresent()) {
val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg); val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg);
if (bt == null) { if (bt == null) {
message.reply("cannot find toggle. Toggles:\n" + togglesString.get()); DPUtils.reply(message, null, "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe();
return true; return true;
} }
final boolean add; final boolean add;
@ -80,7 +83,7 @@ public class ChannelconCommand extends ICommand2DC {
//1 1 | 0 //1 1 | 0
// XOR // XOR
cc.toggles ^= b.get().flag; cc.toggles ^= b.get().flag;
message.reply("'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")); DPUtils.reply(message, null, "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe();
return true; return true;
} }
@ -88,45 +91,49 @@ public class ChannelconCommand extends ICommand2DC {
public boolean def(Command2DCSender sender, String channelID) { public boolean def(Command2DCSender sender, String channelID) {
val message = sender.getMessage(); val message = sender.getMessage();
if (checkPerms(message)) return true; if (checkPerms(message)) return true;
if (MCChatCustom.hasCustomChat(message.getChannel())) if (MCChatCustom.hasCustomChat(message.getChannelId()))
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); 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(); 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) if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
message.reply("MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /."); DPUtils.reply(message, null, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe();
return true; return true;
} }
val dp = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class); 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); val chp = dp.getAs(TBMCPlayer.class);
if (chp == null) { if (chp == null) {
message.reply("you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>"); DPUtils.reply(message, null, "you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>").subscribe();
return true; return true;
} }
DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor(), message.getChannel(), chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName()); val channel = message.getChannel().block();
DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(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 //Using a fake player with no login/logout, should be fine for this event
String groupid = chan.get().getGroupID(dcp); 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 if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later
message.reply("sorry, you cannot use that Minecraft channel."); DPUtils.reply(message, null, "sorry, you cannot use that Minecraft channel.").subscribe();
return true; return true;
} }
if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well
message.reply("chat rooms are not supported yet."); DPUtils.reply(message, null, "chat rooms are not supported yet.").subscribe();
return true; return true;
} }
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
message.reply("sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm.");
return true; return true;
}*/ //TODO: "Channel admins" that can connect channels? }*/ //TODO: "Channel admins" that can connect channels?
MCChatCustom.addCustomChat(message.getChannel(), groupid, chan.get(), message.getAuthor(), dcp, 0, new HashSet<>()); MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>());
if (chan.get() instanceof ChatRoom) if (chan.get() instanceof ChatRoom)
message.reply("alright, connection made to the room!"); DPUtils.reply(message, null, "alright, connection made to the room!").subscribe();
else else
message.reply("alright, connection made to group `" + groupid + "`!"); DPUtils.reply(message, null, "alright, connection made to group `" + groupid + "`!").subscribe();
return true; return true;
} }
private boolean checkPerms(IMessage message) { @SuppressWarnings("ConstantConditions")
if (!PermissionUtils.hasPermissions(message.getChannel(), message.getAuthor(), Permissions.MANAGE_CHANNEL)) { private boolean checkPerms(Message message) {
message.reply("you need to have manage permissions for this channel!"); if (!message.getAuthorAsMember().block().getBasePermissions().block().contains(Permission.MANAGE_CHANNELS)) {
DPUtils.reply(message, null, "you need to have manage permissions for this channel!").subscribe();
return true; return true;
} }
return false; return false;
@ -140,7 +147,7 @@ public class ChannelconCommand extends ICommand2DC {
"You need to have access to the MC channel and have manage permissions on the Discord channel.", // "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>.", // "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.", // "Call this command from the channel you want to use.", //
"Usage: @" + DiscordPlugin.dc.getOurUser().getName() + " channelcon <mcchannel>", // "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon <mcchannel>", //
"Use the ID (command) of the channel, for example `g` for the global chat.", // "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.", // "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() + ".", // "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", //

View file

@ -1,5 +1,6 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender; import buttondevteam.discordplugin.commands.Command2DCSender;
@ -7,6 +8,7 @@ import buttondevteam.discordplugin.commands.ICommand2DC;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import discord4j.core.object.entity.PrivateChannel;
import lombok.val; import lombok.val;
@CommandClass(helpText = { @CommandClass(helpText = {
@ -20,18 +22,20 @@ public class MCChatCommand extends ICommand2DC {
@Command2.Subcommand @Command2.Subcommand
public boolean def(Command2DCSender sender) { public boolean def(Command2DCSender sender) {
val message = sender.getMessage(); val message = sender.getMessage();
if (!message.getChannel().isPrivate()) { val channel = message.getChannel().block();
message.reply("this command can only be issued in a direct message with the bot."); @SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get();
if (!(channel instanceof PrivateChannel)) {
DPUtils.reply(message, null, "this command can only be issued in a direct message with the bot.").subscribe();
return true; return true;
} }
try (final DiscordPlayer user = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class)) { try (final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class)) {
boolean mcchat = !user.isMinecraftChatEnabled(); boolean mcchat = !user.isMinecraftChatEnabled();
MCChatPrivate.privateMCChat(message.getChannel(), mcchat, message.getAuthor(), user); MCChatPrivate.privateMCChat(channel, mcchat, author, user);
message.reply("Minecraft chat " + (mcchat // DPUtils.reply(message, null, "Minecraft chat " + (mcchat //
? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." // ? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." //
: "disabled.")); : "disabled.")).subscribe();
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Error while setting mcchat for user" + message.getAuthor().getName(), e); TBMCCoreAPI.SendException("Error while setting mcchat for user " + author.getUsername() + "#" + author.getDiscriminator(), e);
} }
return true; return true;
} // TODO: Pin channel switching to indicate the current channel } // TODO: Pin channel switching to indicate the current channel

View file

@ -4,10 +4,11 @@ import buttondevteam.core.component.channel.Channel;
import buttondevteam.core.component.channel.ChatRoom; import buttondevteam.core.component.channel.ChatRoom;
import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.TBMCSystemChatEvent;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import discord4j.core.object.util.Snowflake;
import lombok.NonNull; import lombok.NonNull;
import lombok.val; import lombok.val;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,7 +22,7 @@ public class MCChatCustom {
*/ */
static ArrayList<CustomLMD> lastmsgCustom = new ArrayList<>(); static ArrayList<CustomLMD> lastmsgCustom = new ArrayList<>();
public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, IUser user, DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) { public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
if (mcchannel instanceof ChatRoom) { if (mcchannel instanceof ChatRoom) {
((ChatRoom) mcchannel).joinRoom(dcp); ((ChatRoom) mcchannel).joinRoom(dcp);
if (groupid == null) groupid = mcchannel.getGroupID(dcp); if (groupid == null) groupid = mcchannel.getGroupID(dcp);
@ -30,19 +31,19 @@ public class MCChatCustom {
lastmsgCustom.add(lmd); lastmsgCustom.add(lmd);
} }
public static boolean hasCustomChat(IChannel channel) { public static boolean hasCustomChat(Snowflake channel) {
return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getLongID() == channel.getLongID()); return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getId().asLong() == channel.asLong());
} }
@Nullable @Nullable
public static CustomLMD getCustomChat(IChannel channel) { public static CustomLMD getCustomChat(Snowflake channel) {
return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null); return lastmsgCustom.stream().filter(lmd -> lmd.channel.getId().asLong() == channel.asLong()).findAny().orElse(null);
} }
public static boolean removeCustomChat(IChannel channel) { public static boolean removeCustomChat(Snowflake channel) {
MCChatUtils.lastmsgfromd.remove(channel.getLongID()); MCChatUtils.lastmsgfromd.remove(channel.asLong());
return lastmsgCustom.removeIf(lmd -> { return lastmsgCustom.removeIf(lmd -> {
if (lmd.channel.getLongID() != channel.getLongID()) if (lmd.channel.getId().asLong() != channel.asLong())
return false; return false;
if (lmd.mcchannel instanceof ChatRoom) if (lmd.mcchannel instanceof ChatRoom)
((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp); ((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp);
@ -61,8 +62,8 @@ public class MCChatCustom {
public int toggles; public int toggles;
public Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles; public Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles;
private CustomLMD(@NonNull IChannel channel, @NonNull IUser user, private CustomLMD(@NonNull MessageChannel channel, @NonNull User user,
@NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) { @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
super(channel, user); super(channel, user);
groupID = groupid; groupID = groupid;
this.mcchannel = mcchannel; this.mcchannel = mcchannel;

View file

@ -8,26 +8,26 @@ import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.DiscordSender; import buttondevteam.discordplugin.DiscordSender;
import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.discordplugin.listeners.CommandListener; import buttondevteam.discordplugin.listeners.CommandListener;
import buttondevteam.discordplugin.listeners.CommonListeners;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import buttondevteam.discordplugin.util.Timings;
import buttondevteam.lib.*; import buttondevteam.lib.*;
import buttondevteam.lib.chat.ChatMessage; import buttondevteam.lib.chat.ChatMessage;
import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.lib.player.TBMCPlayer; import buttondevteam.lib.player.TBMCPlayer;
import com.vdurmont.emoji.EmojiParser; import com.vdurmont.emoji.EmojiParser;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.Embed;
import discord4j.core.object.entity.*;
import discord4j.core.object.util.Snowflake;
import discord4j.core.spec.EmbedCreateSpec;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import sx.blah.discord.api.internal.json.objects.EmbedObject; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.MissingPermissionsException;
import java.awt.*; import java.awt.*;
import java.time.Instant; import java.time.Instant;
@ -36,15 +36,16 @@ import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MCChatListener implements Listener { public class MCChatListener implements Listener {
private BukkitTask sendtask; private BukkitTask sendtask;
private LinkedBlockingQueue<AbstractMap.SimpleEntry<TBMCChatEvent, Instant>> sendevents = new LinkedBlockingQueue<>(); private LinkedBlockingQueue<AbstractMap.SimpleEntry<TBMCChatEvent, Instant>> sendevents = new LinkedBlockingQueue<>();
private Runnable sendrunnable; private Runnable sendrunnable;
private static Thread sendthread; private static Thread sendthread;
private final MinecraftChatModule module; private final MinecraftChatModule module;
public MCChatListener(MinecraftChatModule minecraftChatModule) { public MCChatListener(MinecraftChatModule minecraftChatModule) {
@ -52,359 +53,363 @@ public class MCChatListener implements Listener {
} }
@EventHandler // Minecraft @EventHandler // Minecraft
public void onMCChat(TBMCChatEvent ev) { public void onMCChat(TBMCChatEvent ev) {
if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown
return; return;
sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now()));
if (sendtask != null) if (sendtask != null)
return; return;
sendrunnable = () -> { sendrunnable = () -> {
sendthread = Thread.currentThread(); sendthread = Thread.currentThread();
processMCToDiscord(); processMCToDiscord();
if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
}; };
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
} }
private void processMCToDiscord() { private void processMCToDiscord() {
try { try {
TBMCChatEvent e; TBMCChatEvent e;
Instant time; Instant time;
val se = sendevents.take(); // Wait until an element is available val se = sendevents.take(); // Wait until an element is available
e = se.getKey(); e = se.getKey();
time = se.getValue(); time = se.getValue();
final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName().get()) + "] " // final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName().get()) + "] " //
+ ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().substring(0, 1) + "]") // + ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().substring(0, 1) + "]") //
+ (DPUtils.sanitizeStringNoEscape(e.getSender() instanceof Player // + (DPUtils.sanitizeStringNoEscape(ThorpeUtils.getDisplayName(e.getSender())));
? ((Player) e.getSender()).getDisplayName() // val color = e.getChannel().Color().get();
: e.getSender().getName())); final Consumer<EmbedCreateSpec> embed = ecs -> {
val color = e.getChannel().Color().get(); ecs.setDescription(e.getMessage()).setColor(new Color(color.getRed(),
final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer) color.getGreen(), color.getBlue()));
.withDescription(e.getMessage()).withColor(new Color(color.getRed(), if (e.getSender() instanceof Player)
color.getGreen(), color.getBlue())); DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(),
// embed.appendField("Channel", ((e.getSender() instanceof DiscordSenderBase ? "d|" : "") "https://tbmcplugins.github.io/profile.html?type=minecraft&id="
// + DiscordPlugin.sanitizeString(e.getChannel().DisplayName)), false); + ((Player) e.getSender()).getUniqueId());
if (e.getSender() instanceof Player) else if (e.getSender() instanceof DiscordSenderBase)
DPUtils.embedWithHead( ecs.setAuthor(authorPlayer, "https://tbmcplugins.github.io/profile.html?type=discord&id=" // TODO: Constant/method to get URLs like this
embed.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=minecraft&id=" + ((DiscordSenderBase) e.getSender()).getUser().getId().asString(),
+ ((Player) e.getSender()).getUniqueId()), ((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl());
e.getSender().getName()); else
else if (e.getSender() instanceof DiscordSenderBase) DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null);
embed.withAuthorIcon(((DiscordSenderBase) e.getSender()).getUser().getAvatarURL()) ecs.setTimestamp(time);
.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=discord&id=" };
+ ((DiscordSenderBase) e.getSender()).getUser().getStringID()); // TODO: Constant/method to get URLs like this final long nanoTime = System.nanoTime();
// embed.withFooterText(e.getChannel().DisplayName); InterruptibleConsumer<MCChatUtils.LastMsgData> doit = lastmsgdata -> {
embed.withTimestamp(time); if (lastmsgdata.message == null
final long nanoTime = System.nanoTime(); || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null))
InterruptibleConsumer<MCChatUtils.LastMsgData> doit = lastmsgdata -> { || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
final EmbedObject embedObject = embed.build(); || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) {
if (lastmsgdata.message == null || lastmsgdata.message.isDeleted() lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block();
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName()) lastmsgdata.time = nanoTime;
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 lastmsgdata.mcchannel = e.getChannel();
|| !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) { lastmsgdata.content = e.getMessage();
lastmsgdata.message = DiscordPlugin.sendMessageToChannelWait(lastmsgdata.channel, "", } else {
embedObject); // TODO Use ChromaBot API lastmsgdata.content = lastmsgdata.content + "\n"
lastmsgdata.time = nanoTime; + e.getMessage(); // The message object doesn't get updated
lastmsgdata.mcchannel = e.getChannel(); lastmsgdata.message.edit(mes -> mes.setEmbed(embed.andThen(ecs ->
lastmsgdata.content = embedObject.description; ecs.setDescription(lastmsgdata.content)))).block();
} else }
try { };
lastmsgdata.content = embedObject.description = lastmsgdata.content + "\n" // Checks if the given channel is different than where the message was sent from
+ embedObject.description;// The message object doesn't get updated // Or if it was from MC
final MCChatUtils.LastMsgData _lastmsgdata = lastmsgdata; Predicate<Snowflake> isdifferentchannel = id -> !(e.getSender() instanceof DiscordSenderBase)
DPUtils.perform(() -> _lastmsgdata.message.edit("", embedObject)); || ((DiscordSenderBase) e.getSender()).getChannel().getId().asLong() != id.asLong();
} catch (MissingPermissionsException | DiscordException e1) {
TBMCCoreAPI.SendException("An error occurred while editing chat message!", e1);
}
};
// Checks if the given channel is different than where the message was sent from
// Or if it was from MC
Predicate<IChannel> isdifferentchannel = ch -> !(e.getSender() instanceof DiscordSenderBase)
|| ((DiscordSenderBase) e.getSender()).getChannel().getLongID() != ch.getLongID();
if (e.getChannel().isGlobal() if (e.getChannel().isGlobal()
&& (e.isFromCommand() || isdifferentchannel.test(module.chatChannel().get()))) && (e.isFromCommand() || isdifferentchannel.test(module.chatChannel().get())))
doit.accept(MCChatUtils.lastmsgdata == null doit.accept(MCChatUtils.lastmsgdata == null
? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannel().get(), null) ? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono().block(), null)
: MCChatUtils.lastmsgdata); : MCChatUtils.lastmsgdata);
for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) { for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) {
if ((e.isFromCommand() || isdifferentchannel.test(data.channel)) if ((e.isFromCommand() || isdifferentchannel.test(data.channel.getId()))
&& e.shouldSendTo(MCChatUtils.getSender(data.channel, data.user))) && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId(), data.user)))
doit.accept(data); doit.accept(data);
} }
val iterator = MCChatCustom.lastmsgCustom.iterator(); val iterator = MCChatCustom.lastmsgCustom.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
val lmd = iterator.next(); val lmd = iterator.next();
if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel)) //Test if msg is from Discord 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.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 && 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 if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions
doit.accept(lmd); doit.accept(lmd);
else { else {
iterator.remove(); //If the user no longer has permission, remove the connection iterator.remove(); //If the user no longer has permission, remove the connection
DiscordPlugin.sendMessageToChannel(lmd.channel, "The user no longer has permission to view the channel, connection removed."); lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe();
} }
} }
} }
} catch (InterruptedException ex) { //Stop if interrupted anywhere } catch (InterruptedException ex) { //Stop if interrupted anywhere
sendtask.cancel(); sendtask.cancel();
sendtask = null; sendtask = null;
} catch (Exception ex) { } catch (Exception ex) {
TBMCCoreAPI.SendException("Error while sending message to Discord!", ex); TBMCCoreAPI.SendException("Error while sending message to Discord!", ex);
} }
} }
@EventHandler @EventHandler
public void onChatPreprocess(TBMCChatPreprocessEvent event) { public void onChatPreprocess(TBMCChatPreprocessEvent event) {
int start = -1; int start = -1;
while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { while ((start = event.getMessage().indexOf('@', start + 1)) != -1) {
int mid = event.getMessage().indexOf('#', start + 1); int mid = event.getMessage().indexOf('#', start + 1);
if (mid == -1) if (mid == -1)
return; return;
int end_ = event.getMessage().indexOf(' ', mid + 1); int end_ = event.getMessage().indexOf(' ', mid + 1);
if (end_ == -1) if (end_ == -1)
end_ = event.getMessage().length(); end_ = event.getMessage().length();
final int end = end_; final int end = end_;
final int startF = start; final int startF = start;
DiscordPlugin.dc.getUsersByName(event.getMessage().substring(start + 1, mid)).stream() 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))).findAny() .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).blockFirst();
.ifPresent(user -> event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getName() if (user != null) //TODO: Nicknames
+ (event.getMessage().length() > end ? event.getMessage().substring(end) : ""))); // TODO: Add formatting event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getUsername()
start = end; // Skip any @s inside the mention + (event.getMessage().length() > end ? event.getMessage().substring(end) : "")); // TODO: Add formatting
} start = end; // Skip any @s inside the mention
} }
}
// ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender
// Offline public chat......x............................................ // Offline public chat......x............................................
// Online public chat.......x...........................................x // Online public chat.......x...........................................x
// Offline private chat.....x.......................x.................... // Offline private chat.....x.......................x....................
// Online private chat......x.......................x...................x // Online private chat......x.......................x...................x
// If online and enabling private chat, don't login // 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 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 private chat is enabled and joining the server, logout the fake player on highest priority
// If online and disabling private chat, don't logout // If online and disabling private chat, don't logout
// The maps may not contain the senders for UnconnectedSenders // The maps may not contain the senders for UnconnectedSenders
/** /**
* Stop the listener. Any calls to onMCChat will restart it as long as we're not in safe mode. * Stop the listener. Any calls to onMCChat will restart it as long as we're not in safe mode.
* *
* @param wait Wait 5 seconds for the threads to stop * @param wait Wait 5 seconds for the threads to stop
*/ */
public static void stop(boolean wait) { public static void stop(boolean wait) {
if (sendthread != null) sendthread.interrupt(); if (sendthread != null) sendthread.interrupt();
if (recthread != null) recthread.interrupt(); if (recthread != null) recthread.interrupt();
try { try {
if (sendthread != null) { if (sendthread != null) {
sendthread.interrupt(); sendthread.interrupt();
if (wait) if (wait)
sendthread.join(5000); sendthread.join(5000);
} }
if (recthread != null) { if (recthread != null) {
recthread.interrupt(); recthread.interrupt();
if (wait) if (wait)
recthread.join(5000); recthread.join(5000);
} }
MCChatUtils.lastmsgdata = null; MCChatUtils.lastmsgdata = null;
MCChatPrivate.lastmsgPerUser.clear(); MCChatPrivate.lastmsgPerUser.clear();
MCChatCustom.lastmsgCustom.clear(); MCChatCustom.lastmsgCustom.clear();
MCChatUtils.lastmsgfromd.clear(); MCChatUtils.lastmsgfromd.clear();
MCChatUtils.ConnectedSenders.clear(); MCChatUtils.ConnectedSenders.clear();
MCChatUtils.UnconnectedSenders.clear(); MCChatUtils.UnconnectedSenders.clear();
recthread = sendthread = null; recthread = sendthread = null;
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); //This thread shouldn't be interrupted e.printStackTrace(); //This thread shouldn't be interrupted
} }
} }
private BukkitTask rectask; private BukkitTask rectask;
private LinkedBlockingQueue<MessageReceivedEvent> recevents = new LinkedBlockingQueue<>(); private LinkedBlockingQueue<MessageCreateEvent> recevents = new LinkedBlockingQueue<>();
private Runnable recrun; private Runnable recrun;
private static Thread recthread; private static Thread recthread;
// Discord // Discord
public boolean handleDiscord(MessageReceivedEvent ev) { public Mono<Boolean> handleDiscord(MessageCreateEvent ev) {
if (!ComponentManager.isEnabled(MinecraftChatModule.class)) val ret = Mono.just(true);
return false; if (!ComponentManager.isEnabled(MinecraftChatModule.class))
val author = ev.getMessage().getAuthor(); return ret;
final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getChannel()); Timings timings = CommonListeners.timings;
if (ev.getMessage().getChannel().getLongID() != module.chatChannel().get().getLongID() timings.printElapsed("Chat event");
&& !(ev.getMessage().getChannel().isPrivate() && MCChatPrivate.isMinecraftChatEnabled(author.getStringID())) val author = ev.getMessage().getAuthor();
&& !hasCustomChat) final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId());
return false; //Chat isn't enabled on this channel return ev.getMessage().getChannel().filter(channel -> {
if (ev.getMessage().getChannel().isPrivate() //Only in private chat timings.printElapsed("Filter 1");
&& ev.getMessage().getContent().length() < "/mcchat<>".length() return !(ev.getMessage().getChannelId().asLong() != module.chatChannel().get().asLong()
&& ev.getMessage().getContent().replace("/", "") && !(channel instanceof PrivateChannel
.equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat && author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false)
return false; //Allow disabling the chat if needed && !hasCustomChat)); //Chat isn't enabled on this channel
if (CommandListener.runCommand(ev.getMessage(), true)) }).filter(channel -> {
return true; //Allow running commands in chat channels timings.printElapsed("Filter 2");
MCChatUtils.resetLastMessage(ev.getChannel()); return !(channel instanceof PrivateChannel //Only in private chat
recevents.add(ev); && ev.getMessage().getContent().isPresent()
if (rectask != null) && ev.getMessage().getContent().get().length() < "/mcchat<>".length()
return true; && ev.getMessage().getContent().get().replace("/", "")
recrun = () -> { //Don't return in a while loop next time .equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat
recthread = Thread.currentThread(); //Allow disabling the chat if needed
processDiscordToMC(); }).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), channel, true))
if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down //Allow running commands in chat channels
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing .filter(channel -> {
}; MCChatUtils.resetLastMessage(channel);
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing recevents.add(ev);
return true; 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()) //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() { private void processDiscordToMC() {
@val MessageCreateEvent event;
sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent event; try {
try { event = recevents.take();
event = recevents.take(); } catch (InterruptedException e1) {
} catch (InterruptedException e1) { rectask.cancel();
rectask.cancel(); return;
return; }
} val sender = event.getMessage().getAuthor().orElse(null);
val sender = event.getMessage().getAuthor(); String dmessage = event.getMessage().getContent().orElse("");
String dmessage = event.getMessage().getContent(); try {
try { final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender);
final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannel(), sender); val user = dsender.getChromaUser();
val user = dsender.getChromaUser();
for (IUser u : event.getMessage().getMentions()) { for (User u : event.getMessage().getUserMentions().toIterable()) { //TODO: Role mentions
dmessage = dmessage.replace(u.mention(false), "@" + u.getName()); // TODO: IG Formatting dmessage = dmessage.replace(u.getMention(), "@" + u.getUsername()); // TODO: IG Formatting
final String nick = u.getNicknameForGuild(DiscordPlugin.mainServer); val m = u.asMember(DiscordPlugin.mainServer.getId()).block();
dmessage = dmessage.replace(u.mention(true), "@" + (nick != null ? nick : u.getName())); if (m != null) {
} final String nick = m.getDisplayName();
for (IChannel ch : event.getMessage().getChannelMentions()) { dmessage = dmessage.replace(m.getNicknameMention(), "@" + nick);
dmessage = dmessage.replace(ch.mention(), "#" + ch.getName()); // TODO: IG Formatting }
} }
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 = 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(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up
Function<String, String> getChatMessage = msg -> // Function<String, String> getChatMessage = msg -> //
msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage()
.getAttachments().stream().map(IMessage.Attachment::getUrl).collect(Collectors.joining("\n")) .getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n"))
: ""); : "");
MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getChannel()); MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getMessage().getChannelId());
boolean react = false; boolean react = false;
if (dmessage.startsWith("/")) { // Ingame command val sendChannel = event.getMessage().getChannel().block();
DPUtils.perform(() -> { boolean isPrivate = sendChannel instanceof PrivateChannel;
if (!event.getMessage().isDeleted() && !event.getChannel().isPrivate()) if (dmessage.startsWith("/")) { // Ingame command
event.getMessage().delete(); if (!isPrivate)
}); event.getMessage().delete().subscribe();
final String cmd = dmessage.substring(1); final String cmd = dmessage.substring(1);
final String cmdlowercased = cmd.toLowerCase(); final String cmdlowercased = cmd.toLowerCase();
if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream()
.noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) {
// Command not whitelisted // Command not whitelisted
dsender.sendMessage("Sorry, you can only access these commands:\n" dsender.sendMessage("Sorry, you can only access these commands:\n"
+ module.whitelistedCommands().get().stream().map(uc -> "/" + uc) + module.whitelistedCommands().get().stream().map(uc -> "/" + uc)
.collect(Collectors.joining(", ")) .collect(Collectors.joining(", "))
+ (user.getConnectedID(TBMCPlayer.class) == null + (user.getConnectedID(TBMCPlayer.class) == null
? "\nTo access your commands, first please connect your accounts, using /connect in " ? "\nTo access your commands, first please connect your accounts, using /connect in "
+ DPUtils.botmention() + DPUtils.botmention()
+ "\nThen y" + "\nThen y"
: "\nY") : "\nY")
+ "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!");
return; return;
} }
val ev = new TBMCCommandPreprocessEvent(dsender, dmessage); val ev = new TBMCCommandPreprocessEvent(dsender, dmessage);
Bukkit.getPluginManager().callEvent(ev); Bukkit.getPluginManager().callEvent(ev);
if (ev.isCancelled()) if (ev.isCancelled())
return; return;
int spi = cmdlowercased.indexOf(' '); int spi = cmdlowercased.indexOf(' ');
final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi); final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi);
Optional<Channel> ch = Channel.getChannels() Optional<Channel> ch = Channel.getChannels()
.filter(c -> c.ID.equalsIgnoreCase(topcmd) .filter(c -> c.ID.equalsIgnoreCase(topcmd)
|| (c.IDs().get().length > 0 || (c.IDs().get().length > 0
&& Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); && Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny();
if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync
() -> { //TODO: Better handling... () -> { //TODO: Better handling...
val channel = user.channel(); val channel = user.channel();
val chtmp = channel.get(); val chtmp = channel.get();
if (clmd != null) { if (clmd != null) {
channel.set(clmd.mcchannel); //Hack to send command in the channel channel.set(clmd.mcchannel); //Hack to send command in the channel
} //TODO: Permcheck isn't implemented for commands } //TODO: Permcheck isn't implemented for commands
VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd);
Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased); Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased);
if (clmd != null) if (clmd != null)
channel.set(chtmp); channel.set(chtmp);
}); });
else { else {
Channel chc = ch.get(); Channel chc = ch.get();
if (!chc.isGlobal() && !event.getMessage().getChannel().isPrivate()) if (!chc.isGlobal() && !isPrivate)
dsender.sendMessage( dsender.sendMessage(
"You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels."); "You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels.");
else { else {
if (spi == -1) // Switch channels if (spi == -1) // Switch channels
{ {
val channel = dsender.getChromaUser().channel(); val channel = dsender.getChromaUser().channel();
val oldch = channel.get(); val oldch = channel.get();
if (oldch instanceof ChatRoom) if (oldch instanceof ChatRoom)
((ChatRoom) oldch).leaveRoom(dsender); ((ChatRoom) oldch).leaveRoom(dsender);
if (!oldch.ID.equals(chc.ID)) { if (!oldch.ID.equals(chc.ID)) {
channel.set(chc); channel.set(chc);
if (chc instanceof ChatRoom) if (chc instanceof ChatRoom)
((ChatRoom) chc).joinRoom(dsender); ((ChatRoom) chc).joinRoom(dsender);
} else } else
channel.set(Channel.GlobalChat); channel.set(Channel.GlobalChat);
dsender.sendMessage("You're now talking in: " dsender.sendMessage("You're now talking in: "
+ DPUtils.sanitizeString(channel.get().DisplayName().get())); + DPUtils.sanitizeString(channel.get().DisplayName().get()));
} else { // Send single message } else { // Send single message
final String msg = cmd.substring(spi + 1); final String msg = cmd.substring(spi + 1);
val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true); val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true);
if (clmd == null) if (clmd == null)
TBMCChatAPI.SendChatMessage(cmb.build(), chc); TBMCChatAPI.SendChatMessage(cmb.build(), chc);
else else
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc); TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc);
react = true; react = true;
} }
} }
} }
} else {// Not a command } else {// Not a command
if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0
&& !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) { && !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) {
val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp)
: dsender.getChromaUser().channel().get().getRTR(dsender); : dsender.getChromaUser().channel().get().getRTR(dsender);
TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr, TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr,
(dsender instanceof Player ? ((Player) dsender).getDisplayName() (dsender instanceof Player ? ((Player) dsender).getDisplayName()
: dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL);
} } else {
else { val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false);
val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); if (clmd != null)
if (clmd != null) TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel);
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); else
else TBMCChatAPI.SendChatMessage(cmb.build());
TBMCChatAPI.SendChatMessage(cmb.build()); react = true;
react = true; }
} }
} if (react) {
if (react) { try {
try { val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong());
val lmfd = MCChatUtils.lastmsgfromd.get(event.getChannel().getLongID()); if (lmfd != null) {
if (lmfd != null) { lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // Remove it no matter what, we know it's there 99.99% of the time
DPUtils.perform(() -> lmfd.removeReaction(DiscordPlugin.dc.getOurUser(), }
DiscordPlugin.DELIVERED_REACTION)); // 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);
} catch (Exception e) { }
TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e); MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage());
} event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe();
MCChatUtils.lastmsgfromd.put(event.getChannel().getLongID(), event.getMessage()); }
DPUtils.perform(() -> event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION)); } catch (Exception e) {
} TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e);
} catch (Exception e) { }
TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e); }
}
}
@FunctionalInterface @FunctionalInterface
private interface InterruptibleConsumer<T> { private interface InterruptibleConsumer<T> {
void accept(T value) throws TimeoutException, InterruptedException; void accept(T value) throws TimeoutException, InterruptedException;
} }
} }

View file

@ -1,17 +1,14 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.player.TBMCPlayer; import buttondevteam.lib.player.TBMCPlayer;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.PrivateChannel;
import discord4j.core.object.entity.User;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IPrivateChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.ArrayList; import java.util.ArrayList;
@ -22,27 +19,31 @@ public class MCChatPrivate {
*/ */
static ArrayList<MCChatUtils.LastMsgData> lastmsgPerUser = new ArrayList<>(); static ArrayList<MCChatUtils.LastMsgData> lastmsgPerUser = new ArrayList<>();
public static boolean privateMCChat(IChannel channel, boolean start, IUser user, DiscordPlayer dp) { public static boolean privateMCChat(MessageChannel channel, boolean start, User user, DiscordPlayer dp) {
TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); TBMCPlayer mcp = dp.getAs(TBMCPlayer.class);
if (mcp != null) { // If the accounts aren't connected, can't make a connected sender if (mcp != null) { // If the accounts aren't connected, can't make a connected sender
val p = Bukkit.getPlayer(mcp.getUUID()); val p = Bukkit.getPlayer(mcp.getUUID());
val op = Bukkit.getOfflinePlayer(mcp.getUUID()); val op = Bukkit.getOfflinePlayer(mcp.getUUID());
val mcm = ComponentManager.getIfEnabled(MinecraftChatModule.class);
if (start) { if (start) {
val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName()); val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName(), mcm);
MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender); MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender);
if (p == null)// Player is offline - If the player is online, that takes precedence if (p == null)// Player is offline - If the player is online, that takes precedence
callEventSync(new PlayerJoinEvent(sender, "")); MCChatUtils.callLoginEvents(sender);
} else { } else {
val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel, user); val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user);
if (p == null)// Player is offline - If the player is online, that takes precedence assert sender != null;
callEventSync(new PlayerQuitEvent(sender, "")); if (p == null // 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, true);
sender.setLoggedIn(false);
} }
} // ---- PermissionsEx warning is normal on logout ---- } // ---- PermissionsEx warning is normal on logout ----
if (!start) if (!start)
MCChatUtils.lastmsgfromd.remove(channel.getLongID()); MCChatUtils.lastmsgfromd.remove(channel.getId().asLong());
return start // return start //
? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs ? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs
: lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); : lastmsgPerUser.removeIf(lmd -> lmd.channel.getId().asLong() == channel.getId().asLong());
} }
public static boolean isMinecraftChatEnabled(DiscordPlayer dp) { public static boolean isMinecraftChatEnabled(DiscordPlayer dp) {
@ -51,18 +52,16 @@ public class MCChatPrivate {
public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this
return lastmsgPerUser.stream() return lastmsgPerUser.stream()
.anyMatch(lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(did)); .anyMatch(lmd -> ((PrivateChannel) lmd.channel)
.getRecipientIds().stream().anyMatch(u -> u.asString().equals(did)));
} }
public static void logoutAll() { public static void logoutAll() {
for (val entry : MCChatUtils.ConnectedSenders.entrySet()) for (val entry : MCChatUtils.ConnectedSenders.entrySet())
for (val valueEntry : entry.getValue().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 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.callEventExcludingSome(new PlayerQuitEvent(valueEntry.getValue(), "")); //This is sync MCChatUtils.callLogoutEvent(valueEntry.getValue(), false); //This is sync
MCChatUtils.ConnectedSenders.clear(); MCChatUtils.ConnectedSenders.clear();
} }
private static void callEventSync(Event event) {
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> MCChatUtils.callEventExcludingSome(event));
}
} }

View file

@ -1,10 +1,13 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.ComponentManager; import buttondevteam.core.ComponentManager;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.*;
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule; import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.TBMCSystemChatEvent;
import com.google.common.collect.Sets;
import discord4j.core.object.entity.*;
import discord4j.core.object.util.Snowflake;
import io.netty.util.collection.LongObjectHashMap; import io.netty.util.collection.LongObjectHashMap;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.experimental.var; import lombok.experimental.var;
@ -13,16 +16,20 @@ import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.event.Event; import org.bukkit.event.Event;
import org.bukkit.event.HandlerList; 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.AuthorNagException;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredListener; import org.bukkit.plugin.RegisteredListener;
import sx.blah.discord.handle.obj.IChannel; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.InetAddress;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -34,23 +41,22 @@ public class MCChatUtils {
/** /**
* May contain P&lt;DiscordID&gt; as key for public chat * May contain P&lt;DiscordID&gt; as key for public chat
*/ */
public static final HashMap<String, HashMap<IChannel, DiscordSender>> UnconnectedSenders = new HashMap<>(); public static final HashMap<String, HashMap<Snowflake, DiscordSender>> UnconnectedSenders = new HashMap<>();
public static final HashMap<String, HashMap<IChannel, DiscordConnectedPlayer>> ConnectedSenders = new HashMap<>(); public static final HashMap<String, HashMap<Snowflake, DiscordConnectedPlayer>> ConnectedSenders = new HashMap<>();
/** /**
* May contain P&lt;DiscordID&gt; as key for public chat * May contain P&lt;DiscordID&gt; as key for public chat
*/ */
public static final HashMap<String, HashMap<IChannel, DiscordPlayerSender>> OnlineSenders = new HashMap<>(); public static final HashMap<String, HashMap<Snowflake, DiscordPlayerSender>> OnlineSenders = new HashMap<>();
static @Nullable LastMsgData lastmsgdata; static @Nullable LastMsgData lastmsgdata;
static LongObjectHashMap<IMessage> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks static LongObjectHashMap<Message> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks
private static MinecraftChatModule module; private static MinecraftChatModule module;
private static HashMap<Class<? extends Event>, HashSet<String>> staticExcludedPlugins = new HashMap<>();
public static void updatePlayerList() { public static void updatePlayerList() {
if (notEnabled()) return; if (notEnabled()) return;
DPUtils.performNoWait(() -> { if (lastmsgdata != null)
if (lastmsgdata != null) updatePL(lastmsgdata);
updatePL(lastmsgdata); MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL);
MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL);
});
} }
private static boolean notEnabled() { private static boolean notEnabled() {
@ -64,55 +70,60 @@ public class MCChatUtils {
} }
private static void updatePL(LastMsgData lmd) { private static void updatePL(LastMsgData lmd) {
String topic = lmd.channel.getTopic(); if (!(lmd.channel instanceof TextChannel)) {
if (topic == null || topic.length() == 0) TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId(),
new Exception("The channel isn't a (guild) text channel."));
return;
}
String topic = ((TextChannel) lmd.channel).getTopic().orElse("");
if (topic.length() == 0)
topic = ".\n----\nMinecraft chat\n----\n."; topic = ".\n----\nMinecraft chat\n----\n.";
String[] s = topic.split("\\n----\\n"); String[] s = topic.split("\\n----\\n");
if (s.length < 3) if (s.length < 3)
return; return;
s[0] = Bukkit.getOnlinePlayers().size() + " player" + (Bukkit.getOnlinePlayers().size() != 1 ? "s" : "") s[0] = Bukkit.getOnlinePlayers().size() + " player" + (Bukkit.getOnlinePlayers().size() != 1 ? "s" : "")
+ " online"; + " online";
s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream()
.map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", "));
lmd.channel.changeTopic(String.join("\n----\n", s)); ((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait
} }
public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<IChannel, T>> senders, public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<Snowflake, T>> senders,
IUser user, T sender) { User user, T sender) {
return addSender(senders, user.getStringID(), sender); return addSender(senders, user.getId().asString(), sender);
} }
public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<IChannel, T>> senders, public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<Snowflake, T>> senders,
String did, T sender) { String did, T sender) {
var map = senders.get(did); var map = senders.get(did);
if (map == null) if (map == null)
map = new HashMap<>(); map = new HashMap<>();
map.put(sender.getChannel(), sender); map.put(sender.getChannel().getId(), sender);
senders.put(did, map); senders.put(did, map);
return sender; return sender;
} }
public static <T extends DiscordSenderBase> T getSender(HashMap<String, HashMap<IChannel, T>> senders, public static <T extends DiscordSenderBase> T getSender(HashMap<String, HashMap<Snowflake, T>> senders,
IChannel channel, IUser user) { Snowflake channel, User user) {
var map = senders.get(user.getStringID()); var map = senders.get(user.getId().asString());
if (map != null) if (map != null)
return map.get(channel); return map.get(channel);
return null; return null;
} }
public static <T extends DiscordSenderBase> T removeSender(HashMap<String, HashMap<IChannel, T>> senders, public static <T extends DiscordSenderBase> T removeSender(HashMap<String, HashMap<Snowflake, T>> senders,
IChannel channel, IUser user) { Snowflake channel, User user) {
var map = senders.get(user.getStringID()); var map = senders.get(user.getId().asString());
if (map != null) if (map != null)
return map.remove(channel); return map.remove(channel);
return null; return null;
} }
public static void forAllMCChat(Consumer<IChannel> action) { public static void forAllMCChat(Consumer<Mono<MessageChannel>> action) {
if (notEnabled()) return; if (notEnabled()) return;
action.accept(module.chatChannel().get()); action.accept(module.chatChannelMono());
for (LastMsgData data : MCChatPrivate.lastmsgPerUser) for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
action.accept(data.channel); action.accept(Mono.just(data.channel));
// lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat
} }
@ -123,11 +134,11 @@ public class MCChatUtils {
* @param toggle The toggle to check * @param toggle The toggle to check
* @param hookmsg Whether the message is also sent from the hook * @param hookmsg Whether the message is also sent from the hook
*/ */
public static void forCustomAndAllMCChat(Consumer<IChannel> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { public static void forCustomAndAllMCChat(Consumer<Mono<MessageChannel>> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
if (notEnabled()) return; if (notEnabled()) return;
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
forAllMCChat(action); forAllMCChat(action);
final Consumer<MCChatCustom.CustomLMD> customLMDConsumer = cc -> action.accept(cc.channel); final Consumer<MCChatCustom.CustomLMD> customLMDConsumer = cc -> action.accept(Mono.just(cc.channel));
if (toggle == null) if (toggle == null)
MCChatCustom.lastmsgCustom.forEach(customLMDConsumer); MCChatCustom.lastmsgCustom.forEach(customLMDConsumer);
else else
@ -141,7 +152,7 @@ public class MCChatUtils {
* @param sender The sender to check perms of or null to send to all that has it toggled * @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 toggle The toggle to check or null to send to all allowed
*/ */
public static void forAllowedCustomMCChat(Consumer<IChannel> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { public static void forAllowedCustomMCChat(Consumer<Mono<MessageChannel>> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) {
if (notEnabled()) return; if (notEnabled()) return;
MCChatCustom.lastmsgCustom.stream().filter(clmd -> { 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 //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple
@ -150,7 +161,7 @@ public class MCChatUtils {
if (sender == null) if (sender == null)
return true; return true;
return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)); return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender));
}).forEach(cc -> action.accept(cc.channel)); //TODO: Send error messages on channel connect }).forEach(cc -> action.accept(Mono.just(cc.channel))); //TODO: Send error messages on channel connect
} }
/** /**
@ -161,42 +172,42 @@ public class MCChatUtils {
* @param toggle The toggle to check or null to send to all allowed * @param toggle The toggle to check or null to send to all allowed
* @param hookmsg Whether the message is also sent from the hook * @param hookmsg Whether the message is also sent from the hook
*/ */
public static void forAllowedCustomAndAllMCChat(Consumer<IChannel> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { public static void forAllowedCustomAndAllMCChat(Consumer<Mono<MessageChannel>> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
if (notEnabled()) return; if (notEnabled()) return;
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
forAllMCChat(action); forAllMCChat(action);
forAllowedCustomMCChat(action, sender, toggle); forAllowedCustomMCChat(action, sender, toggle);
} }
public static Consumer<IChannel> send(String message) { public static Consumer<Mono<MessageChannel>> send(String message) {
return ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(message)); return ch -> ch.flatMap(mc -> mc.createMessage(DPUtils.sanitizeString(message))).subscribe();
} }
public static void forAllowedMCChat(Consumer<IChannel> action, TBMCSystemChatEvent event) { public static void forAllowedMCChat(Consumer<Mono<MessageChannel>> action, TBMCSystemChatEvent event) {
if (notEnabled()) return; if (notEnabled()) return;
if (event.getChannel().isGlobal()) if (event.getChannel().isGlobal())
action.accept(module.chatChannel().get()); action.accept(module.chatChannelMono());
for (LastMsgData data : MCChatPrivate.lastmsgPerUser) for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
if (event.shouldSendTo(getSender(data.channel, data.user))) if (event.shouldSendTo(getSender(data.channel.getId(), data.user)))
action.accept(data.channel); action.accept(Mono.just(data.channel)); //TODO: Only store ID?
MCChatCustom.lastmsgCustom.stream().filter(clmd -> { MCChatCustom.lastmsgCustom.stream().filter(clmd -> {
if (!clmd.brtoggles.contains(event.getTarget())) if (!clmd.brtoggles.contains(event.getTarget()))
return false; return false;
return event.shouldSendTo(clmd.dcp); return event.shouldSendTo(clmd.dcp);
}).map(clmd -> clmd.channel).forEach(action); }).map(clmd -> Mono.just(clmd.channel)).forEach(action);
} }
/** /**
* This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. * 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(IChannel channel, final IUser author) { static DiscordSenderBase getSender(Snowflake channel, final User author) {
//noinspection OptionalGetWithoutIsPresent //noinspection OptionalGetWithoutIsPresent
return Stream.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239 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(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(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.ofNullable(getSender(UnconnectedSenders, channel, author)), //
() -> Optional.of(addSender(UnconnectedSenders, author, () -> Optional.of(addSender(UnconnectedSenders, author,
new DiscordSender(author, channel)))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get();
} }
/** /**
@ -205,15 +216,15 @@ public class MCChatUtils {
* *
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
*/ */
public static void resetLastMessage(IChannel channel) { public static void resetLastMessage(Channel channel) {
if (notEnabled()) return; if (notEnabled()) return;
if (channel.getLongID() == module.chatChannel().get().getLongID()) { if (channel.getId().asLong() == module.chatChannel().get().asLong()) {
(lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannel().get(), null) (lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null)
: lastmsgdata).message = null; : lastmsgdata).message = null;
return; return;
} // Don't set the whole object to null, the player and channel information should be preserved } // Don't set the whole object to null, the player and channel information should be preserved
for (LastMsgData data : channel.isPrivate() ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) { for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) {
if (data.channel.getLongID() == channel.getLongID()) { if (data.channel.getId().asLong() == channel.getId().asLong()) {
data.message = null; data.message = null;
return; return;
} }
@ -221,9 +232,23 @@ public class MCChatUtils {
//If it gets here, it's sending a message to a non-chat channel //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) { public static void callEventExcludingSome(Event event) {
if (notEnabled()) return; if (notEnabled()) return;
callEventExcluding(event, false, module.excludedPlugins().get()); 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);
} }
/** /**
@ -284,13 +309,59 @@ public class MCChatUtils {
} }
} }
/**
* 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);
DPUtils.getLogger().info(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);
DPUtils.getLogger().info(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord");
}
static void callEventSync(Event event) {
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event));
}
@RequiredArgsConstructor @RequiredArgsConstructor
public static class LastMsgData { public static class LastMsgData {
public IMessage message; public Message message;
public long time; public long time;
public String content; public String content;
public final IChannel channel; public final MessageChannel channel;
public Channel mcchannel; public buttondevteam.core.component.channel.Channel mcchannel;
public final IUser user; public final User user;
} }
} }

View file

@ -1,11 +1,12 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.*;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.player.*; import buttondevteam.lib.player.*;
import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.CommandSource;
import discord4j.core.object.entity.Role;
import discord4j.core.object.util.Snowflake;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import net.ess3.api.events.AfkStatusChangeEvent; import net.ess3.api.events.AfkStatusChangeEvent;
@ -17,16 +18,14 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result; import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.BroadcastMessageEvent; import org.bukkit.event.server.BroadcastMessageEvent;
import sx.blah.discord.handle.obj.IRole; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException; import java.util.Objects;
import sx.blah.discord.util.MissingPermissionsException; import java.util.Optional;
@RequiredArgsConstructor @RequiredArgsConstructor
class MCListener implements Listener { class MCListener implements Listener {
@ -36,9 +35,11 @@ class MCListener implements Listener {
public void onPlayerLogin(PlayerLoginEvent e) { public void onPlayerLogin(PlayerLoginEvent e) {
if (e.getResult() != Result.ALLOWED) if (e.getResult() != Result.ALLOWED)
return; return;
if (e.getPlayer() instanceof DiscordConnectedPlayer)
return;
MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny()
.ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerQuitEvent(dcp, ""))); .ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false));
} }
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
@ -49,11 +50,11 @@ class MCListener implements Listener {
final Player p = e.getPlayer(); final Player p = e.getPlayer();
DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class); DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class);
if (dp != null) { if (dp != null) {
val user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); val user = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).block();
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
new DiscordPlayerSender(user, user.getOrCreatePMChannel(), p)); new DiscordPlayerSender(user, Objects.requireNonNull(user).getPrivateChannel().block(), p)); //TODO: Don't block
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
new DiscordPlayerSender(user, module.chatChannel().get(), p)); //Stored per-channel new DiscordPlayerSender(user, module.chatChannelMono().block(), p)); //Stored per-channel
} }
final String message = e.GetPlayer().PlayerName().get() + " joined the game"; final String message = e.GetPlayer().PlayerName().get() + " joined the game";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
@ -67,10 +68,10 @@ class MCListener implements Listener {
return; // Only care about real users return; // Only care about real users
MCChatUtils.OnlineSenders.entrySet() MCChatUtils.OnlineSenders.entrySet()
.removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId()))); .removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId())));
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin,
() -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) () -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream())
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny()
.ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerJoinEvent(dcp, "")))); .ifPresent(MCChatUtils::callLoginEvents));
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
ChromaBot.getInstance()::updatePlayerList, 5); ChromaBot.getInstance()::updatePlayerList, 5);
final String message = e.GetPlayer().PlayerName().get() + " left the game"; final String message = e.GetPlayer().PlayerName().get() + " left the game";
@ -99,38 +100,34 @@ class MCListener implements Listener {
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false); MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false);
} }
private ConfigData<IRole> muteRole() { private ConfigData<Mono<Role>> muteRole() {
return DPUtils.roleData(module.getConfig(), "muteRole", "Muted"); return DPUtils.roleData(module.getConfig(), "muteRole", "Muted");
} }
@EventHandler @EventHandler
public void onPlayerMute(MuteStatusChangeEvent e) { public void onPlayerMute(MuteStatusChangeEvent e) {
try { final Mono<Role> role = muteRole().get();
DPUtils.performNoWait(() -> { if (role == null) return;
final IRole role = muteRole().get(); final CommandSource source = e.getAffected().getSource();
if (role == null) return; if (!source.isPlayer())
final CommandSource source = e.getAffected().getSource(); return;
if (!source.isPlayer()) final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class)
return; .getAs(DiscordPlayer.class);
final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) if (p == null) return;
.getAs(DiscordPlayer.class); DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID()))
if (p == null) return; .flatMap(user -> user.asMember(DiscordPlugin.mainServer.getId()))
final IUser user = DiscordPlugin.dc.getUserByID( .flatMap(user -> role.flatMap(r -> {
Long.parseLong(p.getDiscordID()));
if (e.getValue()) if (e.getValue())
user.addRole(role); user.addRole(r.getId());
else else
user.removeRole(role); user.removeRole(r.getId());
val modlog = module.modlogChannel().get(); val modlog = module.modlogChannel().get();
String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getName(); String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getUsername() + "#" + user.getDiscriminator();
if (modlog != null)
DiscordPlugin.sendMessageToChannel(modlog, msg);
DPUtils.getLogger().info(msg); DPUtils.getLogger().info(msg);
}); if (modlog != null)
} catch (DiscordException | MissingPermissionsException ex) { return modlog.flatMap(ch -> ch.createMessage(msg));
TBMCCoreAPI.SendException("Failed to give/take Muted role to player " + e.getAffected().getName() + "!", return Mono.empty();
ex); })).subscribe();
}
} }
@EventHandler @EventHandler
@ -148,8 +145,9 @@ class MCListener implements Listener {
String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName() String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName()
: event.getSender().getName(); : event.getSender().getName();
//Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO //Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO
val yeehaw = DiscordPlugin.mainServer.getEmojiByName("YEEHAW"); DiscordPlugin.mainServer.getEmojis().filter(e -> "YEEHAW".equals(e.getName()))
MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw != null ? " <:YEEHAW:" + yeehaw.getStringID() + ">s" : " YEEHAWs"))); .take(1).singleOrEmpty().map(Optional::of).defaultIfEmpty(Optional.empty()).subscribe(yeehaw ->
MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw.map(guildEmoji -> " <:YEEHAW:" + guildEmoji.getId().asString() + ">s").orElse(" YEEHAWs")))));
} }
@EventHandler @EventHandler

View file

@ -1,18 +1,23 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.MainPlugin;
import buttondevteam.core.component.channel.Channel; import buttondevteam.core.component.channel.Channel;
import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.playerfaker.perm.LPInjector;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.util.Snowflake;
import lombok.Getter; import lombok.Getter;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import sx.blah.discord.handle.obj.IChannel; import reactor.core.publisher.Mono;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
@ -23,11 +28,12 @@ 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. * 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 class MinecraftChatModule extends Component<DiscordPlugin> {
private @Getter MCChatListener listener; private @Getter
MCChatListener listener;
public MCChatListener getListener() { //It doesn't want to generate /*public MCChatListener getListener() { //It doesn't want to generate
return listener; return listener; - And now ButtonProcessor didn't look beyond this - return instead of continue...
} }*/
/** /**
* 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! * 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!
@ -40,33 +46,46 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
/** /**
* The channel to use as the public Minecraft chat - everything public gets broadcasted here * The channel to use as the public Minecraft chat - everything public gets broadcasted here
*/ */
public ConfigData<IChannel> chatChannel() { public ConfigData<Snowflake> chatChannel() {
return DPUtils.channelData(getConfig(), "chatChannel", 239519012529111040L); return DPUtils.snowflakeData(getConfig(), "chatChannel", 239519012529111040L);
}
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 * The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute
*/ */
public ConfigData<IChannel> modlogChannel() { public ReadOnlyConfigData<Mono<MessageChannel>> modlogChannel() {
return DPUtils.channelData(getConfig(), "modlogChannel", 283840717275791360L); return DPUtils.channelData(getConfig(), "modlogChannel", 283840717275791360L);
} }
/** /**
* 0 * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here
*/ */
public ConfigData<String[]> excludedPlugins() { public ConfigData<String[]> excludedPlugins() {
return getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"}); return 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() {
return getConfig().getData("allowFakePlayerTeleports", false);
}
@Override @Override
protected void enable() { protected void enable() {
if (DPUtils.disableIfConfigError(this, chatChannel())) return; if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono()))
return;
listener = new MCChatListener(this); listener = new MCChatListener(this);
DiscordPlugin.dc.getDispatcher().registerListener(listener);
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin()); TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin());
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled
getPlugin().getManager().registerCommand(new MCChatCommand()); getPlugin().getManager().registerCommand(new MCChatCommand());
getPlugin().getManager().registerCommand(new ChannelconCommand()); getPlugin().getManager().registerCommand(new ChannelconCommand(this));
val chcons = getConfig().getConfig().getConfigurationSection("chcons"); val chcons = getConfig().getConfig().getConfigurationSection("chcons");
if (chcons == null) //Fallback to old place if (chcons == null) //Fallback to old place
@ -76,20 +95,28 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
for (val chconkey : chconkeys) { for (val chconkey : chconkeys) {
val chcon = chcons.getConfigurationSection(chconkey); val chcon = chcons.getConfigurationSection(chconkey);
val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny(); val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny();
val ch = DiscordPlugin.dc.getChannelByID(chcon.getLong("chid")); val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block();
val did = chcon.getLong("did"); val did = chcon.getLong("did");
val user = DiscordPlugin.dc.fetchUser(did); val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block();
val groupid = chcon.getString("groupid"); val groupid = chcon.getString("groupid");
val toggles = chcon.getInt("toggles"); val toggles = chcon.getInt("toggles");
val brtoggles = chcon.getStringList("brtoggles"); val brtoggles = chcon.getStringList("brtoggles");
if (!mcch.isPresent() || ch == null || user == null || groupid == null) if (!mcch.isPresent() || ch == null || user == null || groupid == null)
continue; continue;
Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase) Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase)
val dcp = new DiscordConnectedPlayer(user, ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname")); val dcp = new DiscordConnectedPlayer(user, (MessageChannel) ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this);
MCChatCustom.addCustomChat(ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet())); MCChatCustom.addCustomChat((MessageChannel) ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet()));
}); });
} }
} }
try {
new LPInjector(MainPlugin.Instance);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e);
} catch (NoClassDefFoundError e) {
getPlugin().getLogger().info("No LuckPerms, not injecting");
}
} }
@Override @Override
@ -97,10 +124,10 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
val chcons = MCChatCustom.getCustomChats(); val chcons = MCChatCustom.getCustomChats();
val chconsc = getConfig().getConfig().createSection("chcons"); val chconsc = getConfig().getConfig().createSection("chcons");
for (val chcon : chcons) { for (val chcon : chcons) {
val chconc = chconsc.createSection(chcon.channel.getStringID()); val chconc = chconsc.createSection(chcon.channel.getId().asString());
chconc.set("mcchid", chcon.mcchannel.ID); chconc.set("mcchid", chcon.mcchannel.ID);
chconc.set("chid", chcon.channel.getLongID()); chconc.set("chid", chcon.channel.getId().asLong());
chconc.set("did", chcon.user.getLongID()); chconc.set("did", chcon.user.getId().asLong());
chconc.set("mcuid", chcon.dcp.getUniqueId().toString()); chconc.set("mcuid", chcon.dcp.getUniqueId().toString());
chconc.set("mcname", chcon.dcp.getName()); chconc.set("mcname", chcon.dcp.getName());
chconc.set("groupid", chcon.groupID); chconc.set("groupid", chcon.groupID);

View file

@ -1,44 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.TBMCPlayer;
import buttondevteam.lib.player.TBMCPlayerBase;
import org.bukkit.entity.Player;
@CommandClass(modOnly = false, path = "accept")
public class AcceptMCCommand extends DiscordMCCommandBase {
@Override
public String[] GetHelpText(String alias) {
return new String[] { //
"§6---- 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", //
"Usage: /" + alias + " accept" //
};
}
@Override
public boolean OnCommand(Player player, String alias, String[] args) {
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);
dp.save();
mcp.save();
ConnectCommand.WaitingToConnect.remove(player.getName());
MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed
player.sendMessage("§bAccounts connected.");
return true;
}
}

View file

@ -1,32 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.lib.chat.CommandClass;
import org.bukkit.entity.Player;
@CommandClass(modOnly = false, path = "decline")
public class DeclineMCCommand extends DiscordMCCommandBase {
@Override
public String[] GetHelpText(String alias) {
return new String[] { //
"§6---- 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", //
"Usage: /" + alias + " decline" //
};
}
@Override
public boolean OnCommand(Player player, String alias, String[] args) {
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;
}
}

View file

@ -0,0 +1,131 @@
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.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) {
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);
dp.save();
mcp.save();
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) {
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 reset."
})
public void reload(CommandSender sender) {
if (DiscordPlugin.plugin.tryReloadConfig())
sender.sendMessage("§bConfig reloaded.");
else
sender.sendMessage("§cFailed to reload config.");
}
public static boolean resetting = false;
@Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = {
"Reset ChromaBot", //
"This command disables and then enables the plugin." //
})
public void reset(CommandSender sender) {
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
resetting = true; //Turned off after sending enable message (ReadyEvent)
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("§bReset finished!");
});
}
@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) {
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);
}
}
}

View file

@ -1,9 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.PlayerCommandBase;
@CommandClass(modOnly = false, path = "discord")
public abstract class DiscordMCCommandBase extends PlayerCommandBase {
}

View file

@ -1,26 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord reload")
public class ReloadMCCommand extends TBMCCommandBase {
@Override
public boolean OnCommand(CommandSender sender, String alias, String[] args) {
if (DiscordPlugin.plugin.tryReloadConfig())
sender.sendMessage("§bConfig reloaded."); //TODO: Convert to new command system
else
sender.sendMessage("§cFailed to reload config.");
return true;
}
@Override
public String[] GetHelpText(String alias) {
return new String[]{
"Reload",
"Reloads the config. To apply some changes, you may need to also run /discord reset."
};
}
}

View file

@ -1,35 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord reset", modOnly = true)
public class ResetMCCommand extends TBMCCommandBase { //Not player-only, so not using DiscordMCCommandBase
public static boolean resetting = false;
@Override
public boolean OnCommand(CommandSender sender, String s, String[] strings) {
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
resetting = true; //Turned off after sending enable message (ReadyEvent)
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("§bReset finished!");
});
return true;
}
@Override
public String[] GetHelpText(String s) {
return new String[]{ //
"§6---- Reset ChromaBot ----", //
"This command disables and then enables the plugin." //
};
}
}

View file

@ -1,20 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.commands.VersionCommand;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord version")
public class VersionMCCommand extends TBMCCommandBase {
@Override
public boolean OnCommand(CommandSender commandSender, String s, String[] strings) {
commandSender.sendMessage(VersionCommand.getVersion());
return true;
}
@Override
public String[] GetHelpText(String s) {
return VersionCommand.getVersion(); //Heh
}
}

View file

@ -1,6 +1,10 @@
package buttondevteam.discordplugin.playerfaker; package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.bukkit.*; import org.bukkit.*;
@ -11,8 +15,6 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.metadata.MetadataValue; import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.*; import java.util.*;
@ -20,10 +22,11 @@ import java.util.*;
@Setter @Setter
@SuppressWarnings("deprecated") @SuppressWarnings("deprecated")
public abstract class DiscordEntity extends DiscordSenderBase implements Entity { public abstract class DiscordEntity extends DiscordSenderBase implements Entity {
protected DiscordEntity(IUser user, IChannel channel, int entityId, UUID uuid) { protected DiscordEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) {
super(user, channel); super(user, channel);
this.entityId = entityId; this.entityId = entityId;
uniqueId = uuid; uniqueId = uuid;
this.module = module;
} }
private HashMap<String, MetadataValue> metadata = new HashMap<String, MetadataValue>(); private HashMap<String, MetadataValue> metadata = new HashMap<String, MetadataValue>();
@ -34,6 +37,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity
private EntityDamageEvent lastDamageCause; private EntityDamageEvent lastDamageCause;
private final Set<String> scoreboardTags = new HashSet<String>(); private final Set<String> scoreboardTags = new HashSet<String>();
private final UUID uniqueId; private final UUID uniqueId;
private final MinecraftChatModule module;
@Override @Override
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
@ -42,7 +46,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity
@Override @Override
public List<MetadataValue> getMetadata(String metadataKey) { public List<MetadataValue> getMetadata(String metadataKey) {
return Arrays.asList(metadata.get(metadataKey)); // Who needs multiple data anyways return Collections.singletonList(metadata.get(metadataKey)); // Who needs multiple data anyways
} }
@Override @Override
@ -91,31 +95,35 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity
@Override @Override
public boolean teleport(Location location) { public boolean teleport(Location location) {
this.location = location; if (module.allowFakePlayerTeleports().get())
this.location = location;
return true; return true;
} }
@Override @Override
public boolean teleport(Location location, TeleportCause cause) { public boolean teleport(Location location, TeleportCause cause) {
this.location = location; if (module.allowFakePlayerTeleports().get())
this.location = location;
return true; return true;
} }
@Override @Override
public boolean teleport(Entity destination) { public boolean teleport(Entity destination) {
this.location = destination.getLocation(); if (module.allowFakePlayerTeleports().get())
this.location = destination.getLocation();
return true; return true;
} }
@Override @Override
public boolean teleport(Entity destination, TeleportCause cause) { public boolean teleport(Entity destination, TeleportCause cause) {
this.location = destination.getLocation(); if (module.allowFakePlayerTeleports().get())
this.location = destination.getLocation();
return true; return true;
} }
@Override @Override
public List<Entity> getNearbyEntities(double x, double y, double z) { public List<Entity> getNearbyEntities(double x, double y, double z) {
return Arrays.asList(); return Collections.emptyList();
} }
@Override @Override
@ -163,7 +171,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity
@Override @Override
public List<Entity> getPassengers() { public List<Entity> getPassengers() {
return Arrays.asList(); return Collections.emptyList();
} }
@Override @Override

View file

@ -1,5 +1,8 @@
package buttondevteam.discordplugin.playerfaker; package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@ -8,14 +11,12 @@ import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.inventory.*; import org.bukkit.inventory.*;
import org.bukkit.inventory.InventoryView.Property; import org.bukkit.inventory.InventoryView.Property;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.UUID; import java.util.UUID;
public abstract class DiscordHumanEntity extends DiscordLivingEntity implements HumanEntity { public abstract class DiscordHumanEntity extends DiscordLivingEntity implements HumanEntity {
protected DiscordHumanEntity(IUser user, IChannel channel, int entityId, UUID uuid) { protected DiscordHumanEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) {
super(user, channel, entityId, uuid); super(user, channel, entityId, uuid, module);
} }
private PlayerInventory inv = new DiscordPlayerInventory(this); private PlayerInventory inv = new DiscordPlayerInventory(this);

View file

@ -1,5 +1,8 @@
package buttondevteam.discordplugin.playerfaker; package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.bukkit.Location; import org.bukkit.Location;
@ -16,15 +19,13 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.*; import java.util.*;
public abstract class DiscordLivingEntity extends DiscordEntity implements LivingEntity { public abstract class DiscordLivingEntity extends DiscordEntity implements LivingEntity {
protected DiscordLivingEntity(IUser user, IChannel channel, int entityId, UUID uuid) { protected DiscordLivingEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) {
super(user, channel, entityId, uuid); super(user, channel, entityId, uuid, module);
} }
private @Getter EntityEquipment equipment = new DiscordEntityEquipment(this); private @Getter EntityEquipment equipment = new DiscordEntityEquipment(this);

View file

@ -0,0 +1,240 @@
package buttondevteam.discordplugin.playerfaker.perm;
import buttondevteam.core.MainPlugin;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer;
import buttondevteam.lib.TBMCCoreAPI;
import me.lucko.luckperms.bukkit.LPBukkitBootstrap;
import me.lucko.luckperms.bukkit.LPBukkitPlugin;
import me.lucko.luckperms.bukkit.inject.dummy.DummyPermissibleBase;
import me.lucko.luckperms.bukkit.inject.permissible.LPPermissible;
import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.locale.message.Message;
import me.lucko.luckperms.common.model.User;
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 LPBukkitPlugin plugin;
private BukkitConnectionListener connectionListener;
private Set<UUID> deniedLogin;
private Field detectedCraftBukkitOfflineMode;
private Method printCraftBukkitOfflineModeError;
private Field PERMISSIBLE_BASE_ATTACHMENTS_FIELD;
private Method convertAndAddAttachments;
private Method getActive;
private Method setOldPermissible;
private Method getOldPermissible;
public LPInjector(MainPlugin mp) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException {
LPBukkitBootstrap bs = (LPBukkitBootstrap) Bukkit.getPluginManager().getPlugin("LuckPerms");
Field field = LPBukkitBootstrap.class.getDeclaredField("plugin");
field.setAccessible(true);
plugin = (LPBukkitPlugin) field.get(bs);
MCChatUtils.addStaticExcludedPlugin(PlayerLoginEvent.class, "LuckPerms");
MCChatUtils.addStaticExcludedPlugin(PlayerQuitEvent.class, "LuckPerms");
field = LPBukkitPlugin.class.getDeclaredField("connectionListener");
field.setAccessible(true);
connectionListener = (BukkitConnectionListener) field.get(plugin);
field = connectionListener.getClass().getDeclaredField("deniedLogin");
field.setAccessible(true);
//noinspection unchecked
deniedLogin = (Set<UUID>) field.get(connectionListener);
field = connectionListener.getClass().getDeclaredField("detectedCraftBukkitOfflineMode");
field.setAccessible(true);
detectedCraftBukkitOfflineMode = field;
printCraftBukkitOfflineModeError = connectionListener.getClass().getDeclaredMethod("printCraftBukkitOfflineModeError");
printCraftBukkitOfflineModeError.setAccessible(true);
//PERMISSIBLE_FIELD = DiscordFakePlayer.class.getDeclaredField("perm");
//PERMISSIBLE_FIELD.setAccessible(true); //Hacking my own plugin, while we're at it
PERMISSIBLE_BASE_ATTACHMENTS_FIELD = PermissibleBase.class.getDeclaredField("attachments");
PERMISSIBLE_BASE_ATTACHMENTS_FIELD.setAccessible(true);
convertAndAddAttachments = LPPermissible.class.getDeclaredMethod("convertAndAddAttachments", Collection.class);
convertAndAddAttachments.setAccessible(true);
getActive = LPPermissible.class.getDeclaredMethod("getActive");
getActive.setAccessible(true);
setOldPermissible = LPPermissible.class.getDeclaredMethod("setOldPermissible", PermissibleBase.class);
setOldPermissible.setAccessible(true);
getOldPermissible = LPPermissible.class.getDeclaredMethod("getOldPermissible");
getOldPermissible.setAccessible(true);
TBMCCoreAPI.RegisterEventsForExceptions(this, mp);
}
//Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener
@EventHandler(priority = EventPriority.LOWEST)
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 DiscordFakePlayer))
return; //Normal players must be handled by the plugin
final DiscordFakePlayer player = (DiscordFakePlayer) e.getPlayer();
if (plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) {
plugin.getLogger().info("Processing login for " + player.getUniqueId() + " - " + player.getName());
}
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) {
deniedLogin.add(player.getUniqueId());
if (!connectionListener.getUniqueConnections().contains(player.getUniqueId())) {
plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() +
" doesn't have data pre-loaded, they have never been processed during pre-login in this session." +
" - denying login.");
try {
if ((Boolean) detectedCraftBukkitOfflineMode.get(connectionListener)) {
printCraftBukkitOfflineModeError.invoke(connectionListener);
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR_CB_OFFLINE_MODE.asString(plugin.getLocaleManager()));
return;
}
} catch (IllegalAccessException | InvocationTargetException ex) {
ex.printStackTrace();
}
} else {
plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() +
" doesn't currently have data pre-loaded, but they have been processed before in this session." +
" - denying login.");
}
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR.asString(plugin.getLocaleManager()));
return;
}
// User instance is there, now we can inject our custom Permissible into the player.
// Care should be taken at this stage to ensure that async tasks which manipulate bukkit data check that the player is still online.
try {
// get the existing PermissibleBase held by the player
PermissibleBase oldPermissible = player.getPerm();
// Make a new permissible for the user
LPPermissible lpPermissible = new LPPermissible(player, user, plugin);
// Inject into the player
inject(player, lpPermissible, oldPermissible);
} catch (Throwable t) {
plugin.getLogger().warn("Exception thrown when setting up permissions for " +
player.getUniqueId() + " - " + player.getName() + " - denying login.");
t.printStackTrace();
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_SETUP_ERROR.asString(plugin.getLocaleManager()));
return;
}
plugin.refreshAutoOp(player, true);
}
// Wait until the last priority to unload, so plugins can still perform permission checks on this event
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent e) {
if (!(e.getPlayer() instanceof DiscordFakePlayer))
return;
final DiscordFakePlayer player = (DiscordFakePlayer) e.getPlayer();
connectionListener.handleDisconnect(player.getUniqueId());
// perform unhooking from bukkit objects 1 tick later.
// this allows plugins listening after us on MONITOR to still have intact permissions data
this.plugin.getBootstrap().getServer().getScheduler().runTaskLaterAsynchronously(this.plugin.getBootstrap(), () -> {
// Remove the custom permissible
try {
uninject(player, true);
} catch (Exception ex) {
ex.printStackTrace();
}
// Handle auto op
if (this.plugin.getConfiguration().get(ConfigKeys.AUTO_OP)) {
player.setOp(false);
}
// remove their contexts cache
this.plugin.getContextManager().onPlayerQuit(player);
}, 1L);
}
//me.lucko.luckperms.bukkit.inject.permissible.PermissibleInjector
private void inject(DiscordFakePlayer player, LPPermissible newPermissible, PermissibleBase oldPermissible) throws IllegalAccessException, InvocationTargetException {
// seems we have already injected into this player.
if (oldPermissible instanceof LPPermissible) {
throw new IllegalStateException("LPPermissible already injected into player " + player.toString());
}
// Move attachments over from the old permissible
//noinspection unchecked
List<PermissionAttachment> attachments = (List<PermissionAttachment>) PERMISSIBLE_BASE_ATTACHMENTS_FIELD.get(oldPermissible);
convertAndAddAttachments.invoke(newPermissible, attachments);
attachments.clear();
oldPermissible.clearPermissions();
// Setup the new permissible
((AtomicBoolean) getActive.invoke(newPermissible)).set(true);
setOldPermissible.invoke(newPermissible, oldPermissible);
// inject the new instance
player.setPerm(newPermissible);
}
private void uninject(DiscordFakePlayer player, boolean dummy) throws Exception {
// gets the players current permissible.
PermissibleBase permissible = player.getPerm();
// only uninject if the permissible was a luckperms one.
if (permissible instanceof LPPermissible) {
LPPermissible lpPermissible = ((LPPermissible) permissible);
// clear all permissions
lpPermissible.clearPermissions();
// set to inactive
((AtomicBoolean) getActive.invoke(lpPermissible)).set(false);
// handle the replacement permissible.
if (dummy) {
// just inject a dummy class. this is used when we know the player is about to quit the server.
player.setPerm(DummyPermissibleBase.INSTANCE);
} else {
PermissibleBase newPb = (PermissibleBase) getOldPermissible.invoke(lpPermissible);
if (newPb == null) {
newPb = new PermissibleBase(player);
}
player.setPerm(newPb);
}
}
}
}

View file

@ -4,17 +4,19 @@ import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData; 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.MessageChannel;
import discord4j.core.object.entity.Role;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent; import reactor.core.publisher.Mono;
import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IRole;
import java.awt.*; import java.awt.*;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -24,7 +26,7 @@ public class GameRoleModule extends Component<DiscordPlugin> {
@Override @Override
protected void enable() { protected void enable() {
getPlugin().getManager().registerCommand(new RoleCommand(this)); getPlugin().getManager().registerCommand(new RoleCommand(this));
GameRoles = DiscordPlugin.mainServer.getRoles().stream().filter(this::isGameRole).map(IRole::getName).collect(Collectors.toList()); GameRoles = DiscordPlugin.mainServer.getRoles().filterWhen(this::isGameRole).map(Role::getName).collect(Collectors.toList()).block();
} }
@Override @Override
@ -32,7 +34,7 @@ public class GameRoleModule extends Component<DiscordPlugin> {
} }
private ConfigData<IChannel> logChannel() { private ReadOnlyConfigData<Mono<MessageChannel>> logChannel() {
return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L); return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L);
} }
@ -43,41 +45,55 @@ public class GameRoleModule extends Component<DiscordPlugin> {
val logChannel = grm.logChannel().get(); val logChannel = grm.logChannel().get();
if (roleEvent instanceof RoleCreateEvent) { if (roleEvent instanceof RoleCreateEvent) {
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
if (roleEvent.getRole().isDeleted() || !grm.isGameRole(roleEvent.getRole())) Role role=((RoleCreateEvent) roleEvent).getRole();
return; //Deleted or not a game role grm.isGameRole(role).flatMap(b -> {
GameRoles.add(roleEvent.getRole().getName()); if (!b)
if (logChannel != null) return Mono.empty(); //Deleted or not a game role
DiscordPlugin.sendMessageToChannel(logChannel, "Added " + roleEvent.getRole().getName() + " as game role. If you don't want this, change the role's color from the default."); 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 default."));
return Mono.empty();
}).subscribe();
}, 100); }, 100);
} else if (roleEvent instanceof RoleDeleteEvent) { } else if (roleEvent instanceof RoleDeleteEvent) {
if (GameRoles.remove(roleEvent.getRole().getName()) && logChannel != null) Role role=((RoleDeleteEvent) roleEvent).getRole().orElse(null);
DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + roleEvent.getRole().getName() + " as a game role."); if(role==null) 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) { } else if (roleEvent instanceof RoleUpdateEvent) {
val event = (RoleUpdateEvent) roleEvent; val event = (RoleUpdateEvent) roleEvent;
if (!grm.isGameRole(event.getNewRole())) { if(!event.getOld().isPresent()) {
if (GameRoles.remove(event.getOldRole().getName()) && logChannel != null) DPUtils.getLogger().warning("Old role not stored, cannot update game role!");
DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + event.getOldRole().getName() + " as a game role because it's color changed."); return;
} else {
if (GameRoles.contains(event.getOldRole().getName()) && event.getOldRole().getName().equals(event.getNewRole().getName()))
return;
boolean removed = GameRoles.remove(event.getOldRole().getName()); //Regardless of whether it was a game role
GameRoles.add(event.getNewRole().getName()); //Add it because it has no color
if (logChannel != null) {
if (removed)
DiscordPlugin.sendMessageToChannel(logChannel, "Changed game role from " + event.getOldRole().getName() + " to " + event.getNewRole().getName() + ".");
else
DiscordPlugin.sendMessageToChannel(logChannel, "Added " + event.getNewRole().getName() + " as game role because it has the default color.");
}
} }
Role or=event.getOld().get();
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 it's 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 default color."));
}
}
return Mono.empty();
}).subscribe();
} }
} }
private boolean isGameRole(IRole r) { private Mono<Boolean> isGameRole(Role r) {
if (r.getGuild().getLongID() != DiscordPlugin.mainServer.getLongID()) if (r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong())
return false; //Only allow on the main server return Mono.just(false); //Only allow on the main server
val rc = new Color(149, 165, 166, 0); val rc = new Color(149, 165, 166, 0);
return r.getColor().equals(rc) return Mono.just(r.getColor().equals(rc)).filter(b -> b).flatMap(b ->
&& DiscordPlugin.dc.getOurUser().getRolesForGuild(DiscordPlugin.mainServer) DiscordPlugin.dc.getSelf().flatMap(u -> u.asMember(DiscordPlugin.mainServer.getId())).flatMap(m -> m.hasHigherRoles(Collections.singleton(r)))) //Below one of our roles
.stream().anyMatch(or -> r.getPosition() < or.getPosition()); //Below one of our roles .defaultIfEmpty(false);
} }
} }

View file

@ -1,14 +1,13 @@
package buttondevteam.discordplugin.role; package buttondevteam.discordplugin.role;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender; import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.discordplugin.commands.ICommand2DC; import buttondevteam.discordplugin.commands.ICommand2DC;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import discord4j.core.object.entity.Role;
import lombok.val; import lombok.val;
import sx.blah.discord.handle.obj.IRole;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -27,12 +26,12 @@ public class RoleCommand extends ICommand2DC {
"This command adds a role to your account." "This command adds a role to your account."
}) })
public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) { public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) {
final IRole role = checkAndGetRole(sender, rolename); final Role role = checkAndGetRole(sender, rolename);
if (role == null) if (role == null)
return true; return true;
try { try {
DPUtils.perform(() -> sender.getMessage().getAuthor().addRole(role)); sender.getMessage().getAuthorAsMember()
sender.sendMessage("added role."); .subscribe(m -> m.addRole(role.getId()).subscribe(r -> sender.sendMessage("added role.")));
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Error while adding role!", e); TBMCCoreAPI.SendException("Error while adding role!", e);
sender.sendMessage("an error occured while adding the role."); sender.sendMessage("an error occured while adding the role.");
@ -45,12 +44,12 @@ public class RoleCommand extends ICommand2DC {
"This command removes a role from your account." "This command removes a role from your account."
}) })
public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) { public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) {
final IRole role = checkAndGetRole(sender, rolename); final Role role = checkAndGetRole(sender, rolename);
if (role == null) if (role == null)
return true; return true;
try { try {
DPUtils.perform(() -> sender.getMessage().getAuthor().removeRole(role)); sender.getMessage().getAuthorAsMember()
sender.sendMessage("removed role."); .subscribe(m -> m.removeRole(role.getId()).subscribe(r -> sender.sendMessage("removed role.")));
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Error while removing role!", e); TBMCCoreAPI.SendException("Error while removing role!", e);
sender.sendMessage("an error occured while removing the role."); sender.sendMessage("an error occured while removing the role.");
@ -61,9 +60,9 @@ public class RoleCommand extends ICommand2DC {
@Command2.Subcommand @Command2.Subcommand
public void list(Command2DCSender sender) { public void list(Command2DCSender sender) {
sender.sendMessage("list of roles:\n" + grm.GameRoles.stream().sorted().collect(Collectors.joining("\n"))); sender.sendMessage("list of roles:\n" + grm.GameRoles.stream().sorted().collect(Collectors.joining("\n")));
} }
private IRole checkAndGetRole(Command2DCSender sender, String rolename) { private Role checkAndGetRole(Command2DCSender sender, String rolename) {
String rname = rolename; String rname = rolename;
if (!grm.GameRoles.contains(rolename)) { //If not found as-is, correct case if (!grm.GameRoles.contains(rolename)) { //If not found as-is, correct case
val orn = grm.GameRoles.stream().filter(r -> r.equalsIgnoreCase(rolename)).findAny(); val orn = grm.GameRoles.stream().filter(r -> r.equalsIgnoreCase(rolename)).findAny();
@ -73,18 +72,23 @@ public class RoleCommand extends ICommand2DC {
return null; return null;
} }
rname = orn.get(); rname = orn.get();
} }
final List<IRole> roles = DiscordPlugin.mainServer.getRolesByName(rname); val frname = rname;
if (roles.size() == 0) { final List<Role> roles = DiscordPlugin.mainServer.getRoles().filter(r -> r.getName().equals(frname)).collectList().block();
sender.sendMessage("the specified role cannot be found on Discord! Removing from the list."); if (roles == null) {
grm.GameRoles.remove(rolename); sender.sendMessage("an error occured.");
return null; return null;
} }
if (roles.size() > 1) { if (roles.size() == 0) {
sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?"); sender.sendMessage("the specified role cannot be found on Discord! Removing from the list.");
return null; grm.GameRoles.remove(rolename);
} return null;
return roles.get(0); }
} 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

@ -0,0 +1,16 @@
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();
}
}