Merge pull request #123 from TBMCPlugins/dev

Fixed many issues, default config values, improvements
This commit is contained in:
Norbi Peti 2020-02-01 20:15:57 +01:00 committed by GitHub
commit bcd7f7b810
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 283 additions and 128 deletions

View file

@ -183,7 +183,7 @@
<dependency> <dependency>
<groupId>com.discord4j</groupId> <groupId>com.discord4j</groupId>
<artifactId>discord4j-core</artifactId> <artifactId>discord4j-core</artifactId>
<version>3.0.10</version> <version>3.0.12</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 --> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency> <dependency>

View file

@ -15,11 +15,17 @@ import lombok.val;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.Optional;
import java.util.TreeSet;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Pattern;
public final class DPUtils { public final class DPUtils {
public static final Pattern URL_PATTERN = Pattern.compile("https?://\\S*");
public static final Pattern FORMAT_PATTERN = Pattern.compile("[*_~]");
public static EmbedCreateSpec embedWithHead(EmbedCreateSpec ecs, String displayname, String playername, String profileUrl) { public static EmbedCreateSpec embedWithHead(EmbedCreateSpec ecs, String displayname, String playername, String profileUrl) {
return ecs.setAuthor(displayname, profileUrl, "https://minotar.net/avatar/" + playername + "/32.png"); return ecs.setAuthor(displayname, profileUrl, "https://minotar.net/avatar/" + playername + "/32.png");
} }
@ -51,7 +57,24 @@ public final class DPUtils {
} }
private static String escape(String message) { private static String escape(String message) {
return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\") + "$1"); //var ts = new TreeSet<>();
var ts = new TreeSet<int[]>(Comparator.comparingInt(a -> a[0])); //Compare the start, then check the end
var matcher = URL_PATTERN.matcher(message);
while (matcher.find())
ts.add(new int[]{matcher.start(), matcher.end()});
matcher = FORMAT_PATTERN.matcher(message);
/*Function<MatchResult, String> aFunctionalInterface = result ->
Optional.ofNullable(ts.floor(new int[]{result.start(), 0})).map(a -> a[1]).orElse(0) < result.start()
? "\\\\" + result.group() : result.group();
return matcher.replaceAll(aFunctionalInterface); //Find nearest URL match and if it's not reaching to the char then escape*/
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, Optional.ofNullable(ts.floor(new int[]{matcher.start(), 0})) //Find a URL start <= our start
.map(a -> a[1]).orElse(-1) < matcher.start() //Check if URL end < our start
? "\\\\" + matcher.group() : matcher.group());
}
matcher.appendTail(sb);
return sb.toString();
} }
public static Logger getLogger() { public static Logger getLogger() {
@ -60,8 +83,8 @@ public final class DPUtils {
return DiscordPlugin.plugin.getLogger(); return DiscordPlugin.plugin.getLogger();
} }
public static ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key, long defID) { public static ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key) {
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) return config.getReadOnlyDataPrimDef(key, 0L, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> 0L); //We can afford to search for the channel in the cache once (instead of using mainServer)
} }
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName) { public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName) {
@ -73,13 +96,16 @@ public final class DPUtils {
*/ */
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName, Mono<Guild> guild) { public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName, Mono<Guild> guild) {
return config.getReadOnlyDataPrimDef(key, defName, name -> { return config.getReadOnlyDataPrimDef(key, defName, name -> {
if (!(name instanceof String)) return Mono.empty(); if (!(name instanceof String) || ((String) name).length() == 0) return Mono.empty();
return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).next(); return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).onErrorResume(e -> {
getLogger().warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage());
return Mono.empty();
}).next();
}, r -> defName); }, r -> defName);
} }
public static ConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) { public static ReadOnlyConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) {
return config.getDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong); return config.getReadOnlyDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong);
} }
/** /**
@ -134,12 +160,27 @@ public final class DPUtils {
return false; return false;
} }
/**
* Send a response in the form of "@User, message". Use Mono.empty() if you don't have a channel object.
*
* @param original The original message to reply to
* @param channel The channel to send the message in, defaults to the original
* @param message The message to send
* @return A mono to send the message
*/
public static Mono<Message> reply(Message original, @Nullable MessageChannel channel, String message) { public static Mono<Message> reply(Message original, @Nullable MessageChannel channel, String message) {
Mono<MessageChannel> ch; Mono<MessageChannel> ch;
if (channel == null) if (channel == null)
ch = original.getChannel(); ch = original.getChannel();
else else
ch = Mono.just(channel); ch = Mono.just(channel);
return reply(original, ch, message);
}
/**
* @see #reply(Message, MessageChannel, String)
*/
public static Mono<Message> reply(Message original, Mono<MessageChannel> ch, String message) {
return ch.flatMap(chan -> chan.createMessage((original.getAuthor().isPresent() return ch.flatMap(chan -> chan.createMessage((original.getAuthor().isPresent()
? original.getAuthor().get().getMention() + ", " : "") + message)); ? original.getAuthor().get().getMention() + ", " : "") + message));
} }
@ -152,7 +193,15 @@ public final class DPUtils {
return "<#" + channelId.asString() + ">"; return "<#" + channelId.asString() + ">";
} }
/**
* Gets a message channel for a config. Returns empty for ID 0.
*
* @param key The config key
* @param id The channel ID
* @return A message channel
*/
public static Mono<MessageChannel> getMessageChannel(String key, Snowflake id) { public static Mono<MessageChannel> getMessageChannel(String key, Snowflake id) {
if (id.asLong() == 0L) return Mono.empty();
return DiscordPlugin.dc.getChannelById(id).onErrorResume(e -> { return DiscordPlugin.dc.getChannelById(id).onErrorResume(e -> {
getLogger().warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage()); getLogger().warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage());
return Mono.empty(); return Mono.empty();

View file

@ -44,7 +44,6 @@ 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.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -56,6 +55,9 @@ public class DiscordPlugin extends ButtonPlugin {
@Getter @Getter
private Command2DC manager; private Command2DC manager;
/**
* The prefix to use with Discord commands like /role. It only works in the bot channel.
*/
private 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);
} }
@ -65,6 +67,9 @@ public class DiscordPlugin extends ButtonPlugin {
return plugin.prefix().get(); return plugin.prefix().get();
} }
/**
* The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
*/
private ConfigData<Optional<Guild>> mainServer() { private ConfigData<Optional<Guild>> mainServer() {
return getIConfig().getDataPrimDef("mainServer", 0L, return getIConfig().getDataPrimDef("mainServer", 0L,
id -> { id -> {
@ -77,12 +82,16 @@ public class DiscordPlugin extends ButtonPlugin {
g -> g.map(gg -> gg.getId().asLong()).orElse(0L)); g -> g.map(gg -> gg.getId().asLong()).orElse(0L));
} }
/**
* The (bot) channel to use for Discord commands like /role.
*/
public ConfigData<Snowflake> commandChannel() { public ConfigData<Snowflake> commandChannel() {
return DPUtils.snowflakeData(getIConfig(), "commandChannel", 239519012529111040L); return DPUtils.snowflakeData(getIConfig(), "commandChannel", 0L);
} }
/** /**
* If the role doesn't exist, then it will only allow for the owner. * The role that allows using mod-only Discord commands.
* If empty (''), then it will only allow for the owner.
*/ */
public ConfigData<Mono<Role>> modRole() { public ConfigData<Mono<Role>> modRole() {
return DPUtils.roleData(getIConfig(), "modRole", "Moderator"); return DPUtils.roleData(getIConfig(), "modRole", "Moderator");
@ -101,6 +110,7 @@ public class DiscordPlugin extends ButtonPlugin {
getLogger().info("Initializing..."); getLogger().info("Initializing...");
plugin = this; plugin = this;
manager = new Command2DC(); manager = new Command2DC();
getCommand2MC().registerCommand(new DiscordMCCommand()); //Register so that the reset command works
String token; String token;
File tokenFile = new File("TBMC", "Token.txt"); File tokenFile = new File("TBMC", "Token.txt");
if (tokenFile.exists()) //Legacy support if (tokenFile.exists()) //Legacy support
@ -114,8 +124,9 @@ public class DiscordPlugin extends ButtonPlugin {
conf.set("token", "Token goes here"); conf.set("token", "Token goes here");
conf.save(privateFile); conf.save(privateFile);
getLogger().severe("Token not found! Set it in private.yml"); getLogger().severe("Token not found! Please set it in private.yml then do /discord reset");
Bukkit.getPluginManager().disablePlugin(this); getLogger().severe("You need to have a bot account to use with your server.");
getLogger().severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.");
return; return;
} }
} }
@ -133,8 +144,8 @@ public class DiscordPlugin extends ButtonPlugin {
//dc.getEventDispatcher().on(DisconnectEvent.class); //dc.getEventDispatcher().on(DisconnectEvent.class);
dc.login().subscribe(); dc.login().subscribe();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e);
Bukkit.getPluginManager().disablePlugin(this); getLogger().severe("You may be able to reset the plugin using /discord reset");
} }
} }
@ -144,10 +155,10 @@ public class DiscordPlugin extends ButtonPlugin {
try { try {
if (mainServer != null) { //This is not the first ready event if (mainServer != null) { //This is not the first ready event
getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections
dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); //Update from the initial presence
return; return;
} }
mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards
getCommand2MC().registerCommand(new DiscordMCCommand()); //Register so that the reset command works
if (mainServer == null) { if (mainServer == null) {
if (event.size() == 0) { if (event.size() == 0) {
getLogger().severe("Main server not found! Invite the bot and do /discord reset"); getLogger().severe("Main server not found! Invite the bot and do /discord reset");
@ -163,7 +174,7 @@ public class DiscordPlugin extends ButtonPlugin {
} }
SafeMode = false; SafeMode = false;
DPUtils.disableIfConfigErrorRes(null, commandChannel(), DPUtils.getMessageChannel(commandChannel())); DPUtils.disableIfConfigErrorRes(null, commandChannel(), DPUtils.getMessageChannel(commandChannel()));
DPUtils.disableIfConfigError(null, modRole()); //Won't disable, just prints the warning here //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());
@ -196,14 +207,6 @@ public class DiscordPlugin extends ButtonPlugin {
getConfig().set("serverup", true); getConfig().set("serverup", true);
saveConfig(); saveConfig();
if (TBMCCoreAPI.IsTestServer() && !Objects.requireNonNull(dc.getSelf().block()).getUsername().toLowerCase().contains("test")) {
TBMCCoreAPI.SendException(
"Won't load because we're in testing mode and not using a separate account.",
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.)"
+ "\nYou can disable test mode in ThorpeCore config."));
Bukkit.getPluginManager().disablePlugin(this);
}
TBMCCoreAPI.SendUnsentExceptions(); TBMCCoreAPI.SendUnsentExceptions();
TBMCCoreAPI.SendUnsentDebugMessages(); TBMCCoreAPI.SendUnsentDebugMessages();

View file

@ -5,6 +5,7 @@ import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin; 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.ComponentMetadata;
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData; import buttondevteam.lib.architecture.ReadOnlyConfigData;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
@ -15,25 +16,26 @@ import com.google.gson.JsonParser;
import discord4j.core.object.entity.Message; import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel; import discord4j.core.object.entity.MessageChannel;
import lombok.val; import lombok.val;
import org.bukkit.configuration.file.YamlConfiguration;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.io.File; /**
* Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts).
*/
@ComponentMetadata(enabledByDefault = false)
public class AnnouncerModule extends Component<DiscordPlugin> { public class AnnouncerModule extends Component<DiscordPlugin> {
/** /**
* Channel to post new posts. * Channel to post new posts.
*/ */
public ReadOnlyConfigData<Mono<MessageChannel>> channel() { public ReadOnlyConfigData<Mono<MessageChannel>> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); return DPUtils.channelData(getConfig(), "channel");
} }
/** /**
* Channel where distinguished (moderator) posts go. * Channel where distinguished (moderator) posts go.
*/ */
public ReadOnlyConfigData<Mono<MessageChannel>> modChannel() { public ReadOnlyConfigData<Mono<MessageChannel>> modChannel() {
return DPUtils.channelData(getConfig(), "modChannel", 239519012529111040L); return DPUtils.channelData(getConfig(), "modChannel");
} }
/** /**
@ -51,7 +53,13 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
return getConfig().getData("lastSeenTime", 0L); return getConfig().getData("lastSeenTime", 0L);
} }
private static final String SubredditURL = "https://www.reddit.com/r/ChromaGamers"; /**
* The subreddit to pull the posts from
*/
private ConfigData<String> subredditURL() {
return getConfig().getData("subredditURL", "https://www.reddit.com/r/ChromaGamers");
}
private static boolean stop = false; private static boolean stop = false;
@Override @Override
@ -62,11 +70,6 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
if (keepPinned == 0) return; if (keepPinned == 0) return;
Flux<Message> msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages); Flux<Message> msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages);
msgs.subscribe(Message::unpin); msgs.subscribe(Message::unpin);
val yc = YamlConfiguration.loadConfiguration(new File("plugins/DiscordPlugin", "config.yml")); //Name change
if (lastAnnouncementTime().get() == 0) //Load old data
lastAnnouncementTime().set(yc.getLong("lastannouncementtime"));
if (lastSeenTime().get() == 0)
lastSeenTime().set(yc.getLong("lastseentime"));
new Thread(this::AnnouncementGetterThreadMethod).start(); new Thread(this::AnnouncementGetterThreadMethod).start();
} }
@ -82,7 +85,7 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
Thread.sleep(10000); Thread.sleep(10000);
continue; continue;
} }
String body = TBMCCoreAPI.DownloadString(SubredditURL + "/new/.json?limit=10"); String body = TBMCCoreAPI.DownloadString(subredditURL().get() + "/new/.json?limit=10");
JsonArray json = new JsonParser().parse(body).getAsJsonObject().get("data").getAsJsonObject() JsonArray json = new JsonParser().parse(body).getAsJsonObject().get("data").getAsJsonObject()
.get("children").getAsJsonArray(); .get("children").getAsJsonArray();
StringBuilder msgsb = new StringBuilder(); StringBuilder msgsb = new StringBuilder();

View file

@ -6,6 +6,10 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import lombok.Getter; import lombok.Getter;
/**
* Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone.
* If this component is enabled then these messages will show up on Discord.
*/
public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> { public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> {
private static @Getter boolean hooked = false; private static @Getter boolean hooked = false;

View file

@ -3,6 +3,7 @@ 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 discord4j.core.object.entity.Message;
import discord4j.core.object.entity.User;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
@ -30,4 +31,9 @@ public class Command2DCSender implements Command2Sender {
public void sendMessage(String[] message) { public void sendMessage(String[] message) {
sendMessage(String.join("\n", message)); sendMessage(String.join("\n", message));
} }
@Override
public String getName() {
return message.getAuthor().map(User::getUsername).orElse("Discord");
}
} }

View file

@ -23,6 +23,9 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy.
*/
public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener { 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<>();
@ -84,10 +87,16 @@ public class ExceptionListenerModule extends Component<DiscordPlugin> implements
return Mono.empty(); return Mono.empty();
} }
/**
* The channel to post the errors to.
*/
private ReadOnlyConfigData<Mono<MessageChannel>> channel() { private ReadOnlyConfigData<Mono<MessageChannel>> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); return DPUtils.channelData(getConfig(), "channel");
} }
/**
* The role to ping if an error occurs. Set to empty ('') to disable.
*/
private ConfigData<Mono<Role>> pingRole(Mono<Guild> guild) { private ConfigData<Mono<Role>> pingRole(Mono<Guild> guild) {
return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild); return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild);
} }

View file

@ -26,23 +26,24 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream; import java.util.stream.IntStream;
/** /**
* All kinds of random things.
* The YEEHAW event uses an emoji named :YEEHAW: if available * 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
}; };
/** /**
@ -52,14 +53,14 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
return getConfig().getData("serverReady", () -> new String[]{"when will the server be open", 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 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", "when will the server be finished", "when's the server ready", "when's the server open",
"Vhen vill ze server be open?"}); "vhen vill ze server be open?"});
} }
/** /**
* Answers for a recognized question. Selected randomly. * 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));
} }
private static final Random serverReadyRandom = new Random(); private static final Random serverReadyRandom = new Random();
@ -96,7 +97,7 @@ 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
{ {
DPUtils.reply(message, null, "Stop it. You know the answer.").subscribe(); DPUtils.reply(message, Mono.empty(), "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
@ -108,7 +109,7 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
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(); DPUtils.reply(message, Mono.empty(), fm.serverReadyAnswers().get().get(next)).subscribe();
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;
@ -119,13 +120,19 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
ListC = 0; ListC = 0;
} }
/**
* If all of the people who have this role are online, the bot will post a full house.
*/
private ConfigData<Mono<Role>> fullHouseDevRole(Mono<Guild> guild) { private ConfigData<Mono<Role>> fullHouseDevRole(Mono<Guild> guild) {
return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild); return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild);
} }
/**
* The channel to post the full house to.
*/
private ReadOnlyConfigData<Mono<MessageChannel>> fullHouseChannel() { private ReadOnlyConfigData<Mono<MessageChannel>> fullHouseChannel() {
return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L); return DPUtils.channelData(getConfig(), "fullHouseChannel");
} }
private static long lasttime = 0; private static long lasttime = 0;

View file

@ -62,7 +62,7 @@ public class CommandListener {
try { try {
timings.printElapsed("F"); timings.printElapsed("F");
if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString))
return DPUtils.reply(message, channel, "Unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString) return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString)
.map(m -> false); .map(m -> false);
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e); TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e);

View file

@ -9,12 +9,17 @@ 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.GuildChannel;
import discord4j.core.object.entity.Message; import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel;
import discord4j.core.object.entity.User;
import discord4j.core.object.util.Permission; import discord4j.core.object.util.Permission;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import reactor.core.publisher.Mono;
import javax.annotation.Nullable;
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;
@ -22,6 +27,7 @@ import java.util.Objects;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuppressWarnings("SimplifyOptionalCallChains") //Java 11
@CommandClass(helpText = {"Channel connect", // @CommandClass(helpText = {"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", // "You need to have access to the MC channel and have manage permissions on the Discord channel.", //
@ -36,28 +42,29 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ChannelconCommand extends ICommand2DC { public class ChannelconCommand extends ICommand2DC {
private final MinecraftChatModule module; 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, null)) return true;
if (MCChatCustom.removeCustomChat(message.getChannelId())) if (MCChatCustom.removeCustomChat(message.getChannelId()))
DPUtils.reply(message, null, "channel connection removed.").subscribe(); DPUtils.reply(message, Mono.empty(), "channel connection removed.").subscribe();
else else
DPUtils.reply(message, null, "this channel isn't connected.").subscribe(); DPUtils.reply(message, Mono.empty(), "this channel isn't connected.").subscribe();
return true; return true;
} }
@Command2.Subcommand @Command2.Subcommand
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, null)) return true;
val cc = MCChatCustom.getCustomChat(message.getChannelId()); 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) {
DPUtils.reply(message, null, "toggles:\n" + togglesString.get()).subscribe(); DPUtils.reply(message, Mono.empty(), "toggles:\n" + togglesString.get()).subscribe();
return true; return true;
} }
String arg = toggle.toUpperCase(); String arg = toggle.toUpperCase();
@ -65,7 +72,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) {
DPUtils.reply(message, null, "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe(); DPUtils.reply(message, Mono.empty(), "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe();
return true; return true;
} }
final boolean add; final boolean add;
@ -83,7 +90,7 @@ public class ChannelconCommand extends ICommand2DC {
//1 1 | 0 //1 1 | 0
// XOR // XOR
cc.toggles ^= b.get().flag; cc.toggles ^= b.get().flag;
DPUtils.reply(message, null, "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe(); DPUtils.reply(message, Mono.empty(), "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe();
return true; return true;
} }
@ -94,12 +101,13 @@ public class ChannelconCommand extends ICommand2DC {
sender.sendMessage("channel connection is not allowed on this Minecraft server."); sender.sendMessage("channel connection is not allowed on this Minecraft server.");
return true; return true;
} }
if (checkPerms(message)) return true; val channel = message.getChannel().block();
if (checkPerms(message, channel)) return true;
if (MCChatCustom.hasCustomChat(message.getChannelId())) 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)
DPUtils.reply(message, null, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe(); DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe();
return true; return true;
} }
if (!message.getAuthor().isPresent()) return true; if (!message.getAuthor().isPresent()) return true;
@ -107,19 +115,18 @@ public class ChannelconCommand extends ICommand2DC {
val dp = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); 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) {
DPUtils.reply(message, null, "you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>").subscribe(); DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>").subscribe();
return true; return true;
} }
val channel = message.getChannel().block();
DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module); DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module);
//Using a fake player with no login/logout, should be fine for this event //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
DPUtils.reply(message, null, "sorry, you cannot use that Minecraft channel.").subscribe(); DPUtils.reply(message, channel, "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
DPUtils.reply(message, null, "chat rooms are not supported yet.").subscribe(); DPUtils.reply(message, channel, "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))) {
@ -128,16 +135,23 @@ public class ChannelconCommand extends ICommand2DC {
}*/ //TODO: "Channel admins" that can connect channels? }*/ //TODO: "Channel admins" that can connect channels?
MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, 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)
DPUtils.reply(message, null, "alright, connection made to the room!").subscribe(); DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe();
else else
DPUtils.reply(message, null, "alright, connection made to group `" + groupid + "`!").subscribe(); DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe();
return true; return true;
} }
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
private boolean checkPerms(Message message) { private boolean checkPerms(Message message, @Nullable MessageChannel channel) {
if (!message.getAuthorAsMember().block().getBasePermissions().block().contains(Permission.MANAGE_CHANNELS)) { if (channel == null)
DPUtils.reply(message, null, "you need to have manage permissions for this channel!").subscribe(); channel = message.getChannel().block();
if (!(channel instanceof GuildChannel)) {
DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe();
return true;
}
var perms = ((GuildChannel) channel).getEffectivePermissions(message.getAuthor().map(User::getId).get()).block();
if (!perms.contains(Permission.ADMINISTRATOR) && !perms.contains(Permission.MANAGE_CHANNELS)) {
DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe();
return true; return true;
} }
return false; return false;
@ -145,17 +159,17 @@ public class ChannelconCommand extends ICommand2DC {
@Override @Override
public String[] getHelpText(Method method, Command2.Subcommand ann) { public String[] getHelpText(Method method, Command2.Subcommand ann) {
return new String[]{ // return new String[]{ //
"Channel connect", // "Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", // "You 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: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " 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() + ".", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=" + module.clientID + "&scope=bot&permissions=268509264>" "Invite link: <https://discordapp.com/oauth2/authorize?client_id=" + DiscordPlugin.dc.getApplicationInfo().map(info -> info.getId().asString()).blockOptional().orElse("Unknown") + "&scope=bot&permissions=268509264>"
}; };
} }
} }

View file

@ -33,13 +33,13 @@ public class MCChatCommand extends ICommand2DC {
val channel = message.getChannel().block(); val channel = message.getChannel().block();
@SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get(); @SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get();
if (!(channel instanceof PrivateChannel)) { if (!(channel instanceof PrivateChannel)) {
DPUtils.reply(message, null, "this command can only be issued in a direct message with the bot.").subscribe(); DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe();
return true; return true;
} }
try (final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class)) { try (final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class)) {
boolean mcchat = !user.isMinecraftChatEnabled(); boolean mcchat = !user.isMinecraftChatEnabled();
MCChatPrivate.privateMCChat(channel, mcchat, author, user); MCChatPrivate.privateMCChat(channel, mcchat, author, user);
DPUtils.reply(message, null, "Minecraft chat " + (mcchat // DPUtils.reply(message, channel, "Minecraft chat " + (mcchat //
? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." // ? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." //
: "disabled.")).subscribe(); : "disabled.")).subscribe();
} catch (Exception e) { } catch (Exception e) {

View file

@ -84,13 +84,14 @@ public class MCChatListener implements Listener {
final Consumer<EmbedCreateSpec> embed = ecs -> { final Consumer<EmbedCreateSpec> embed = ecs -> {
ecs.setDescription(e.getMessage()).setColor(new Color(color.getRed(), ecs.setDescription(e.getMessage()).setColor(new Color(color.getRed(),
color.getGreen(), color.getBlue())); color.getGreen(), color.getBlue()));
String url = module.profileURL().get();
if (e.getSender() instanceof Player) if (e.getSender() instanceof Player)
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(),
"https://tbmcplugins.github.io/profile.html?type=minecraft&id=" url.length() > 0 ? url + "?type=minecraft&id="
+ ((Player) e.getSender()).getUniqueId()); + ((Player) e.getSender()).getUniqueId() : null);
else if (e.getSender() instanceof DiscordSenderBase) else if (e.getSender() instanceof DiscordSenderBase)
ecs.setAuthor(authorPlayer, "https://tbmcplugins.github.io/profile.html?type=discord&id=" // TODO: Constant/method to get URLs like this ecs.setAuthor(authorPlayer, url.length() > 0 ? url + "?type=discord&id="
+ ((DiscordSenderBase) e.getSender()).getUser().getId().asString(), + ((DiscordSenderBase) e.getSender()).getUser().getId().asString() : null,
((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl()); ((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl());
else else
DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null); DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null);
@ -101,7 +102,8 @@ public class MCChatListener implements Listener {
if (lastmsgdata.message == null if (lastmsgdata.message == null
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null)) || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null))
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|| !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) { || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)
|| lastmsgdata.content.length() + e.getMessage().length() + 1 > 2048) {
lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block(); lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block();
lastmsgdata.time = nanoTime; lastmsgdata.time = nanoTime;
lastmsgdata.mcchannel = e.getChannel(); lastmsgdata.mcchannel = e.getChannel();
@ -292,6 +294,8 @@ public class MCChatListener implements Listener {
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
dmessage = dmessage.replaceAll("<a?:(\\S+):(\\d+)>", ":$1:"); //We don't need info about the custom emojis, just display their text
Function<String, String> getChatMessage = msg -> // 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(Attachment::getUrl).collect(Collectors.joining("\n")) .getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n"))

View file

@ -1,6 +1,7 @@
package buttondevteam.discordplugin.mcchat; package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.ComponentManager; import buttondevteam.core.ComponentManager;
import buttondevteam.core.MainPlugin;
import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.*;
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule; import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
@ -13,6 +14,7 @@ import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
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.AsyncPlayerPreLoginEvent;
@ -30,6 +32,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.logging.Level; import java.util.logging.Level;
@ -81,13 +84,27 @@ public class MCChatUtils {
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" : "") String gid;
+ " online"; if (lmd instanceof MCChatCustom.CustomLMD)
gid = ((MCChatCustom.CustomLMD) lmd).groupID;
else //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most
gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE; // (Though it's a public chat then rn)
AtomicInteger C = new AtomicInteger();
s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream()
.filter(p -> gid.equals(lmd.mcchannel.getGroupID(p))) //If they can see it
.filter(MCChatUtils::checkEssentials)
.filter(p -> C.incrementAndGet() > 0) //Always true
.map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", "));
s[0] = C + " player" + (C.get() != 1 ? "s" : "") + " online";
((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait ((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait
} }
private static boolean checkEssentials(Player p) {
var ess = MainPlugin.ess;
if (ess == null) return true;
return !ess.getUser(p).isHidden();
}
public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<Snowflake, T>> senders, public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<Snowflake, T>> senders,
User user, T sender) { User user, T sender) {
return addSender(senders, user.getId().asString(), sender); return addSender(senders, user.getId().asString(), sender);

View file

@ -3,7 +3,9 @@ package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.*;
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.TBMCPlayer;
import buttondevteam.lib.player.TBMCPlayerBase;
import buttondevteam.lib.player.TBMCYEEHAWEvent;
import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.CommandSource;
import discord4j.core.object.entity.Role; import discord4j.core.object.entity.Role;
import discord4j.core.object.util.Snowflake; import discord4j.core.object.util.Snowflake;
@ -18,9 +20,7 @@ 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.PlayerCommandSendEvent; import org.bukkit.event.player.*;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result; import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.server.BroadcastMessageEvent; import org.bukkit.event.server.BroadcastMessageEvent;
import org.bukkit.event.server.TabCompleteEvent; import org.bukkit.event.server.TabCompleteEvent;
@ -44,13 +44,13 @@ class MCListener implements Listener {
.ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false)); .ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false));
} }
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(TBMCPlayerJoinEvent e) { public void onPlayerJoin(PlayerJoinEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer) if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Don't show the joined message for the fake player return; // Don't show the joined message for the fake player
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
final Player p = e.getPlayer(); final Player p = e.getPlayer();
DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class); DiscordPlayer dp = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class).getAs(DiscordPlayer.class);
if (dp != null) { if (dp != null) {
DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).flatMap(user -> user.getPrivateChannel().flatMap(chan -> module.chatChannelMono().flatMap(cc -> { DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).flatMap(user -> user.getPrivateChannel().flatMap(chan -> module.chatChannelMono().flatMap(cc -> {
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
@ -60,14 +60,15 @@ class MCListener implements Listener {
return Mono.empty(); return Mono.empty();
}))).subscribe(); }))).subscribe();
} }
final String message = e.GetPlayer().PlayerName().get() + " joined the game"; final String message = e.getJoinMessage();
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
ChromaBot.getInstance().updatePlayerList(); ChromaBot.getInstance().updatePlayerList();
}); });
} }
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLeave(TBMCPlayerQuitEvent e) { public void onPlayerLeave(PlayerQuitEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer) if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Only care about real users return; // Only care about real users
MCChatUtils.OnlineSenders.entrySet() MCChatUtils.OnlineSenders.entrySet()
@ -78,8 +79,9 @@ class MCListener implements Listener {
.ifPresent(MCChatUtils::callLoginEvents)); .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.getQuitMessage();
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
} }
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.HIGHEST)

View file

@ -28,12 +28,7 @@ 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 private @Getter MCChatListener listener;
MCChatListener listener;
/*public MCChatListener getListener() { //It doesn't want to generate
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!
@ -46,8 +41,8 @@ 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<Snowflake> chatChannel() { public ReadOnlyConfigData<Snowflake> chatChannel() {
return DPUtils.snowflakeData(getConfig(), "chatChannel", 239519012529111040L); return DPUtils.snowflakeData(getConfig(), "chatChannel", 0L);
} }
public Mono<MessageChannel> chatChannelMono() { public Mono<MessageChannel> chatChannelMono() {
@ -58,7 +53,7 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
* 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 ReadOnlyConfigData<Mono<MessageChannel>> modlogChannel() { public ReadOnlyConfigData<Mono<MessageChannel>> modlogChannel() {
return DPUtils.channelData(getConfig(), "modlogChannel", 283840717275791360L); return DPUtils.channelData(getConfig(), "modlogChannel");
} }
/** /**
@ -104,13 +99,19 @@ public class MinecraftChatModule extends Component<DiscordPlugin> {
return getConfig().getData("allowPrivateChat", true); return getConfig().getData("allowPrivateChat", true);
} }
String clientID; /**
* If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID.
*/
public ConfigData<String> profileURL() {
return getConfig().getData("profileURL", "");
}
@Override @Override
protected void enable() { protected void enable() {
if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono())) if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono()))
return; return;
DiscordPlugin.dc.getApplicationInfo().subscribe(info -> clientID = info.getId().asString()); /*clientID = DiscordPlugin.dc.getApplicationInfo().blockOptional().map(info->info.getId().asString())
.orElse("Unknown"); //Need to block because otherwise it may not be set in time*/
listener = new MCChatListener(this); listener = new MCChatListener(this);
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

View file

@ -27,6 +27,7 @@ import java.lang.reflect.Method;
public class DiscordMCCommand extends ICommand2MC { public class DiscordMCCommand extends ICommand2MC {
@Command2.Subcommand @Command2.Subcommand
public boolean accept(Player player) { public boolean accept(Player player) {
if (checkSafeMode(player)) return true;
String did = ConnectCommand.WaitingToConnect.get(player.getName()); String did = ConnectCommand.WaitingToConnect.get(player.getName());
if (did == null) { if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord."); player.sendMessage("§cYou don't have a pending connection to Discord.");
@ -45,6 +46,7 @@ public class DiscordMCCommand extends ICommand2MC {
@Command2.Subcommand @Command2.Subcommand
public boolean decline(Player player) { public boolean decline(Player player) {
if (checkSafeMode(player)) return true;
String did = ConnectCommand.WaitingToConnect.remove(player.getName()); String did = ConnectCommand.WaitingToConnect.remove(player.getName());
if (did == null) { if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord."); player.sendMessage("§cYou don't have a pending connection to Discord.");
@ -73,6 +75,10 @@ public class DiscordMCCommand extends ICommand2MC {
}) })
public void reset(CommandSender sender) { public void reset(CommandSender sender) {
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
if (!DiscordPlugin.plugin.tryReloadConfig()) {
sender.sendMessage("§cFailed to reload config so not resetting. Check the console.");
return;
}
resetting = true; //Turned off after sending enable message (ReadyEvent) resetting = true; //Turned off after sending enable message (ReadyEvent)
sender.sendMessage("§bDisabling DiscordPlugin..."); sender.sendMessage("§bDisabling DiscordPlugin...");
Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin); Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin);
@ -97,6 +103,7 @@ public class DiscordMCCommand extends ICommand2MC {
"Shows an invite link to the server" "Shows an invite link to the server"
}) })
public void invite(CommandSender sender) { public void invite(CommandSender sender) {
if (checkSafeMode(sender)) return;
String invi = DiscordPlugin.plugin.inviteLink().get(); String invi = DiscordPlugin.plugin.inviteLink().get();
if (invi.length() > 0) { if (invi.length() > 0) {
sender.sendMessage("§bInvite link: " + invi); sender.sendMessage("§bInvite link: " + invi);
@ -128,4 +135,12 @@ public class DiscordMCCommand extends ICommand2MC {
return super.getHelpText(method, ann); return super.getHelpText(method, ann);
} }
} }
private boolean checkSafeMode(CommandSender sender) {
if (DiscordPlugin.SafeMode) {
sender.sendMessage("§cThe plugin isn't initialized. Check console for details.");
return true;
}
return false;
}
} }

View file

@ -21,6 +21,10 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Automatically collects roles with a certain color (the second to last in the upper row - #95a5a6).
* Users can add these roles to themselves using the /role Discord command.
*/
public class GameRoleModule extends Component<DiscordPlugin> { public class GameRoleModule extends Component<DiscordPlugin> {
public List<String> GameRoles; public List<String> GameRoles;
@ -35,8 +39,11 @@ public class GameRoleModule extends Component<DiscordPlugin> {
} }
/**
* The channel where the bot logs when it detects a role change that results in a new game role or one being removed.
*/
private ReadOnlyConfigData<Mono<MessageChannel>> logChannel() { private ReadOnlyConfigData<Mono<MessageChannel>> logChannel() {
return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L); return DPUtils.channelData(getConfig(), "logChannel");
} }
public static void handleRoleEvent(RoleEvent roleEvent) { public static void handleRoleEvent(RoleEvent roleEvent) {
@ -52,7 +59,7 @@ public class GameRoleModule extends Component<DiscordPlugin> {
return Mono.empty(); //Deleted or not a game role return Mono.empty(); //Deleted or not a game role
GameRoles.add(role.getName()); GameRoles.add(role.getName());
if (logChannel != null) 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 logChannel.flatMap(ch -> ch.createMessage("Added " + role.getName() + " as game role. If you don't want this, change the role's color from the game role color."));
return Mono.empty(); return Mono.empty();
}).subscribe(); }).subscribe();
}, 100); }, 100);
@ -81,7 +88,7 @@ public class GameRoleModule extends Component<DiscordPlugin> {
if (removed) if (removed)
return logChannel.flatMap(ch -> ch.createMessage("Changed game role from " + or.getName() + " to " + event.getCurrent().getName() + ".")); return logChannel.flatMap(ch -> ch.createMessage("Changed game role from " + or.getName() + " to " + event.getCurrent().getName() + "."));
else else
return logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the default color.")); return logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the color of one."));
} }
} }
return Mono.empty(); return Mono.empty();

View file

@ -11,7 +11,6 @@ import lombok.val;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@CommandClass @CommandClass
public class RoleCommand extends ICommand2DC { public class RoleCommand extends ICommand2DC {
@ -62,7 +61,20 @@ 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"))); var sb = new StringBuilder();
boolean b = false;
for (String role : (Iterable<String>) grm.GameRoles.stream().sorted()::iterator) {
sb.append(role);
if (!b)
for (int j = 0; j < Math.max(1, 20 - role.length()); j++)
sb.append(" ");
else
sb.append("\n");
b = !b;
}
if (sb.charAt(sb.length() - 1) != '\n')
sb.append('\n');
sender.sendMessage("list of roles:\n```\n" + sb + "```");
} }
private Role checkAndGetRole(Command2DCSender sender, String rolename) { private Role checkAndGetRole(Command2DCSender sender, String rolename) {

View file

@ -1,8 +1,10 @@
name: Chroma-Discord name: Chroma-Discord
main: buttondevteam.discordplugin.DiscordPlugin main: buttondevteam.discordplugin.DiscordPlugin
version: 1.0 version: '1.0'
author: NorbiPeti author: NorbiPeti
depend: [ChromaCore] depend: [ChromaCore]
softdepend:
- Essentials
commands: commands:
discord: discord:
website: 'https://github.com/TBMCPlugins/DiscordPlugin' website: 'https://github.com/TBMCPlugins/DiscordPlugin'