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>
<groupId>com.discord4j</groupId>
<artifactId>discord4j-core</artifactId>
<version>3.0.10</version>
<version>3.0.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency>

View file

@ -15,11 +15,17 @@ import lombok.val;
import reactor.core.publisher.Mono;
import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.Optional;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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) {
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) {
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() {
@ -60,8 +83,8 @@ public final class DPUtils {
return DiscordPlugin.plugin.getLogger();
}
public static ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key, long defID) {
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 ReadOnlyConfigData<Mono<MessageChannel>> channelData(IHaveConfig config, String key) {
return config.getReadOnlyDataPrimDef(key, 0L, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> 0L); //We can afford to search for the channel in the cache once (instead of using mainServer)
}
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName) {
@ -73,13 +96,16 @@ public final class DPUtils {
*/
public static ReadOnlyConfigData<Mono<Role>> roleData(IHaveConfig config, String key, String defName, Mono<Guild> guild) {
return config.getReadOnlyDataPrimDef(key, defName, name -> {
if (!(name instanceof String)) return Mono.empty();
return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).next();
if (!(name instanceof String) || ((String) name).length() == 0) return Mono.empty();
return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).onErrorResume(e -> {
getLogger().warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage());
return Mono.empty();
}).next();
}, r -> defName);
}
public static ConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) {
return config.getDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong);
public static ReadOnlyConfigData<Snowflake> snowflakeData(IHaveConfig config, String key, long defID) {
return config.getReadOnlyDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong);
}
/**
@ -134,12 +160,27 @@ public final class DPUtils {
return false;
}
/**
* Send a response in the form of "@User, message". Use Mono.empty() if you don't have a channel object.
*
* @param original The original message to reply to
* @param channel The channel to send the message in, defaults to the original
* @param message The message to send
* @return A mono to send the message
*/
public static Mono<Message> reply(Message original, @Nullable MessageChannel channel, String message) {
Mono<MessageChannel> ch;
if (channel == null)
ch = original.getChannel();
else
ch = Mono.just(channel);
return reply(original, ch, message);
}
/**
* @see #reply(Message, MessageChannel, String)
*/
public static Mono<Message> reply(Message original, Mono<MessageChannel> ch, String message) {
return ch.flatMap(chan -> chan.createMessage((original.getAuthor().isPresent()
? original.getAuthor().get().getMention() + ", " : "") + message));
}
@ -152,7 +193,15 @@ public final class DPUtils {
return "<#" + channelId.asString() + ">";
}
/**
* Gets a message channel for a config. Returns empty for ID 0.
*
* @param key The config key
* @param id The channel ID
* @return A message channel
*/
public static Mono<MessageChannel> getMessageChannel(String key, Snowflake id) {
if (id.asLong() == 0L) return Mono.empty();
return DiscordPlugin.dc.getChannelById(id).onErrorResume(e -> {
getLogger().warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage());
return Mono.empty();

View file

@ -44,7 +44,6 @@ import java.awt.*;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@ -56,6 +55,9 @@ public class DiscordPlugin extends ButtonPlugin {
@Getter
private Command2DC manager;
/**
* The prefix to use with Discord commands like /role. It only works in the bot channel.
*/
private ConfigData<Character> prefix() {
return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString);
}
@ -65,6 +67,9 @@ public class DiscordPlugin extends ButtonPlugin {
return plugin.prefix().get();
}
/**
* The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
*/
private ConfigData<Optional<Guild>> mainServer() {
return getIConfig().getDataPrimDef("mainServer", 0L,
id -> {
@ -77,12 +82,16 @@ public class DiscordPlugin extends ButtonPlugin {
g -> g.map(gg -> gg.getId().asLong()).orElse(0L));
}
/**
* The (bot) channel to use for Discord commands like /role.
*/
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() {
return DPUtils.roleData(getIConfig(), "modRole", "Moderator");
@ -101,6 +110,7 @@ public class DiscordPlugin extends ButtonPlugin {
getLogger().info("Initializing...");
plugin = this;
manager = new Command2DC();
getCommand2MC().registerCommand(new DiscordMCCommand()); //Register so that the reset command works
String token;
File tokenFile = new File("TBMC", "Token.txt");
if (tokenFile.exists()) //Legacy support
@ -114,8 +124,9 @@ public class DiscordPlugin extends ButtonPlugin {
conf.set("token", "Token goes here");
conf.save(privateFile);
getLogger().severe("Token not found! Set it in private.yml");
Bukkit.getPluginManager().disablePlugin(this);
getLogger().severe("Token not found! Please set it in private.yml then do /discord reset");
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;
}
}
@ -133,8 +144,8 @@ public class DiscordPlugin extends ButtonPlugin {
//dc.getEventDispatcher().on(DisconnectEvent.class);
dc.login().subscribe();
} catch (Exception e) {
e.printStackTrace();
Bukkit.getPluginManager().disablePlugin(this);
TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e);
getLogger().severe("You may be able to reset the plugin using /discord reset");
}
}
@ -144,10 +155,10 @@ public class DiscordPlugin extends ButtonPlugin {
try {
if (mainServer != null) { //This is not the first ready event
getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections
dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); //Update from the initial presence
return;
}
mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards
getCommand2MC().registerCommand(new DiscordMCCommand()); //Register so that the reset command works
if (mainServer == null) {
if (event.size() == 0) {
getLogger().severe("Main server not found! Invite the bot and do /discord reset");
@ -163,7 +174,7 @@ public class DiscordPlugin extends ButtonPlugin {
}
SafeMode = false;
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 MinecraftChatModule());
@ -196,14 +207,6 @@ public class DiscordPlugin extends ButtonPlugin {
getConfig().set("serverup", true);
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.SendUnsentDebugMessages();

View file

@ -5,6 +5,7 @@ import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ComponentMetadata;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.ReadOnlyConfigData;
import buttondevteam.lib.player.ChromaGamerBase;
@ -15,25 +16,26 @@ import com.google.gson.JsonParser;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.MessageChannel;
import lombok.val;
import org.bukkit.configuration.file.YamlConfiguration;
import reactor.core.publisher.Flux;
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> {
/**
* Channel to post new posts.
*/
public ReadOnlyConfigData<Mono<MessageChannel>> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L);
return DPUtils.channelData(getConfig(), "channel");
}
/**
* Channel where distinguished (moderator) posts go.
*/
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);
}
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;
@Override
@ -62,11 +70,6 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
if (keepPinned == 0) return;
Flux<Message> msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages);
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();
}
@ -82,7 +85,7 @@ public class AnnouncerModule extends Component<DiscordPlugin> {
Thread.sleep(10000);
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()
.get("children").getAsJsonArray();
StringBuilder msgsb = new StringBuilder();

View file

@ -6,6 +6,10 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
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> {
private static @Getter boolean hooked = false;

View file

@ -3,6 +3,7 @@ package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.lib.chat.Command2Sender;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.User;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.val;
@ -30,4 +31,9 @@ public class Command2DCSender implements Command2Sender {
public void sendMessage(String[] 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.stream.Collectors;
/**
* Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy.
*/
public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener {
private List<Throwable> lastthrown = new ArrayList<>();
private List<String> lastsourcemsg = new ArrayList<>();
@ -84,10 +87,16 @@ public class ExceptionListenerModule extends Component<DiscordPlugin> implements
return Mono.empty();
}
/**
* The channel to post the errors to.
*/
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) {
return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild);
}

View file

@ -26,23 +26,24 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* All kinds of random things.
* The YEEHAW event uses an emoji named :YEEHAW: if available
*/
public class FunModule extends Component<DiscordPlugin> implements Listener {
private static final String[] serverReadyStrings = new String[]{"In one week from now", // Ali
"Between now and the heat-death of the universe.", // Ghostise
"Soon™", "Ask again this time next month", // Ghostise
"In about 3 seconds", // Nicolai
"After we finish 8 plugins", // Ali
"Tomorrow.", // Ali
"After one tiiiny feature", // Ali
"Next commit", // Ali
"After we finish strangling Towny", // Ali
"When we kill every *fucking* bug", // Ali
"Once the server stops screaming.", // Ali
"After HL3 comes out", // Ali
"Next time you ask", // Ali
"When will *you* be open?" // Ali
private static final String[] serverReadyStrings = new String[]{"in one week from now", // Ali
"between now and the heat-death of the universe.", // Ghostise
"soon™", "ask again this time next month", // Ghostise
"in about 3 seconds", // Nicolai
"after we finish 8 plugins", // Ali
"tomorrow.", // Ali
"after one tiiiny feature", // Ali
"next commit", // Ali
"after we finish strangling Towny", // Ali
"when we kill every *fucking* bug", // Ali
"once the server stops screaming.", // Ali
"after HL3 comes out", // Ali
"next time you ask", // Ali
"when will *you* be open?" // Ali
};
/**
@ -52,14 +53,14 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
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?"});
"vhen vill ze server be open?"});
}
/**
* Answers for a recognized question. Selected randomly.
*/
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();
@ -96,7 +97,7 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
}
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;
lastlistp = (short) Bukkit.getOnlinePlayers().size();
return true; //Handled
@ -108,7 +109,7 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
if (usableServerReadyStrings.size() == 0)
fm.createUsableServerReadyStrings();
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;
@ -119,13 +120,19 @@ public class FunModule extends Component<DiscordPlugin> implements Listener {
ListC = 0;
}
/**
* If all of the people who have this role are online, the bot will post a full house.
*/
private ConfigData<Mono<Role>> fullHouseDevRole(Mono<Guild> guild) {
return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild);
}
/**
* The channel to post the full house to.
*/
private ReadOnlyConfigData<Mono<MessageChannel>> fullHouseChannel() {
return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L);
return DPUtils.channelData(getConfig(), "fullHouseChannel");
}
private static long lasttime = 0;

View file

@ -62,7 +62,7 @@ public class CommandListener {
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)
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);

View file

@ -9,12 +9,17 @@ import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.TBMCPlayer;
import discord4j.core.object.entity.GuildChannel;
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 lombok.RequiredArgsConstructor;
import lombok.val;
import org.bukkit.Bukkit;
import reactor.core.publisher.Mono;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
@ -22,6 +27,7 @@ import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@SuppressWarnings("SimplifyOptionalCallChains") //Java 11
@CommandClass(helpText = {"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
@ -36,28 +42,29 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class ChannelconCommand extends ICommand2DC {
private final MinecraftChatModule module;
@Command2.Subcommand
public boolean remove(Command2DCSender sender) {
val message = sender.getMessage();
if (checkPerms(message)) return true;
if (checkPerms(message, null)) return true;
if (MCChatCustom.removeCustomChat(message.getChannelId()))
DPUtils.reply(message, null, "channel connection removed.").subscribe();
DPUtils.reply(message, Mono.empty(), "channel connection removed.").subscribe();
else
DPUtils.reply(message, null, "this channel isn't connected.").subscribe();
DPUtils.reply(message, Mono.empty(), "this channel isn't connected.").subscribe();
return true;
}
@Command2.Subcommand
public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) {
val message = sender.getMessage();
if (checkPerms(message)) return true;
if (checkPerms(message, null)) return true;
val cc = MCChatCustom.getCustomChat(message.getChannelId());
if (cc == null)
return respond(sender, "this channel isn't connected.");
Supplier<String> togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n"))
+ "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n"));
if (toggle == null) {
DPUtils.reply(message, null, "toggles:\n" + togglesString.get()).subscribe();
DPUtils.reply(message, Mono.empty(), "toggles:\n" + togglesString.get()).subscribe();
return true;
}
String arg = toggle.toUpperCase();
@ -65,7 +72,7 @@ public class ChannelconCommand extends ICommand2DC {
if (!b.isPresent()) {
val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg);
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;
}
final boolean add;
@ -83,7 +90,7 @@ public class ChannelconCommand extends ICommand2DC {
//1 1 | 0
// XOR
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;
}
@ -94,12 +101,13 @@ public class ChannelconCommand extends ICommand2DC {
sender.sendMessage("channel connection is not allowed on this Minecraft server.");
return true;
}
if (checkPerms(message)) return true;
val channel = message.getChannel().block();
if (checkPerms(message, channel)) return true;
if (MCChatCustom.hasCustomChat(message.getChannelId()))
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.");
val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs().get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny();
if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
DPUtils.reply(message, 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;
}
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 chp = dp.getAs(TBMCPlayer.class);
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;
}
val channel = message.getChannel().block();
DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module);
//Using a fake player with no login/logout, should be fine for this event
String groupid = chan.get().getGroupID(dcp);
if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later
DPUtils.reply(message, null, "sorry, you cannot use that Minecraft channel.").subscribe();
DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe();
return true;
}
if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well
DPUtils.reply(message, null, "chat rooms are not supported yet.").subscribe();
DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe();
return true;
}
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
@ -128,16 +135,23 @@ public class ChannelconCommand extends ICommand2DC {
}*/ //TODO: "Channel admins" that can connect channels?
MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>());
if (chan.get() instanceof ChatRoom)
DPUtils.reply(message, null, "alright, connection made to the room!").subscribe();
DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe();
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;
}
@SuppressWarnings("ConstantConditions")
private boolean checkPerms(Message message) {
if (!message.getAuthorAsMember().block().getBasePermissions().block().contains(Permission.MANAGE_CHANNELS)) {
DPUtils.reply(message, null, "you need to have manage permissions for this channel!").subscribe();
private boolean checkPerms(Message message, @Nullable MessageChannel channel) {
if (channel == null)
channel = message.getChannel().block();
if (!(channel instanceof GuildChannel)) {
DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe();
return true;
}
var perms = ((GuildChannel) channel).getEffectivePermissions(message.getAuthor().map(User::getId).get()).block();
if (!perms.contains(Permission.ADMINISTRATOR) && !perms.contains(Permission.MANAGE_CHANNELS)) {
DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe();
return true;
}
return false;
@ -145,17 +159,17 @@ public class ChannelconCommand extends ICommand2DC {
@Override
public String[] getHelpText(Method method, Command2.Subcommand ann) {
return new String[]{ //
"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
"You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect <mcname>.", //
"Call this command from the channel you want to use.", //
return new String[]{ //
"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
"You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect <mcname>.", //
"Call this command from the channel you want to use.", //
"Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon <mcchannel>", //
"Use the ID (command) of the channel, for example `g` for the global chat.", //
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=" + module.clientID + "&scope=bot&permissions=268509264>"
};
}
"Use the ID (command) of the channel, for example `g` for the global chat.", //
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=" + DiscordPlugin.dc.getApplicationInfo().map(info -> info.getId().asString()).blockOptional().orElse("Unknown") + "&scope=bot&permissions=268509264>"
};
}
}

View file

@ -33,13 +33,13 @@ public class MCChatCommand extends ICommand2DC {
val channel = message.getChannel().block();
@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();
DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe();
return true;
}
try (final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class)) {
boolean mcchat = !user.isMinecraftChatEnabled();
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." //
: "disabled.")).subscribe();
} catch (Exception e) {

View file

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

View file

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

View file

@ -3,7 +3,9 @@ package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.*;
import buttondevteam.lib.TBMCSystemChatEvent;
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 discord4j.core.object.entity.Role;
import discord4j.core.object.util.Snowflake;
@ -18,9 +20,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerCommandSendEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.*;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.server.BroadcastMessageEvent;
import org.bukkit.event.server.TabCompleteEvent;
@ -44,13 +44,13 @@ class MCListener implements Listener {
.ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false));
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(TBMCPlayerJoinEvent e) {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Don't show the joined message for the fake player
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
final Player p = e.getPlayer();
DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class);
DiscordPlayer dp = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class).getAs(DiscordPlayer.class);
if (dp != null) {
DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).flatMap(user -> user.getPrivateChannel().flatMap(chan -> module.chatChannelMono().flatMap(cc -> {
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
@ -60,14 +60,15 @@ class MCListener implements Listener {
return Mono.empty();
}))).subscribe();
}
final String message = e.GetPlayer().PlayerName().get() + " joined the game";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
final String message = e.getJoinMessage();
if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
ChromaBot.getInstance().updatePlayerList();
});
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerLeave(TBMCPlayerQuitEvent e) {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLeave(PlayerQuitEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Only care about real users
MCChatUtils.OnlineSenders.entrySet()
@ -78,8 +79,9 @@ class MCListener implements Listener {
.ifPresent(MCChatUtils::callLoginEvents));
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
ChromaBot.getInstance()::updatePlayerList, 5);
final String message = e.GetPlayer().PlayerName().get() + " left the game";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
final String message = e.getQuitMessage();
if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
}
@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.
*/
public class MinecraftChatModule extends Component<DiscordPlugin> {
private @Getter
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...
}*/
private @Getter MCChatListener listener;
/**
* 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
*/
public ConfigData<Snowflake> chatChannel() {
return DPUtils.snowflakeData(getConfig(), "chatChannel", 239519012529111040L);
public ReadOnlyConfigData<Snowflake> chatChannel() {
return DPUtils.snowflakeData(getConfig(), "chatChannel", 0L);
}
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
*/
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);
}
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
protected void enable() {
if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono()))
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);
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin());
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 {
@Command2.Subcommand
public boolean accept(Player player) {
if (checkSafeMode(player)) return true;
String did = ConnectCommand.WaitingToConnect.get(player.getName());
if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord.");
@ -45,6 +46,7 @@ public class DiscordMCCommand extends ICommand2MC {
@Command2.Subcommand
public boolean decline(Player player) {
if (checkSafeMode(player)) return true;
String did = ConnectCommand.WaitingToConnect.remove(player.getName());
if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord.");
@ -73,6 +75,10 @@ public class DiscordMCCommand extends ICommand2MC {
})
public void reset(CommandSender sender) {
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)
sender.sendMessage("§bDisabling DiscordPlugin...");
Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin);
@ -97,6 +103,7 @@ public class DiscordMCCommand extends ICommand2MC {
"Shows an invite link to the server"
})
public void invite(CommandSender sender) {
if (checkSafeMode(sender)) return;
String invi = DiscordPlugin.plugin.inviteLink().get();
if (invi.length() > 0) {
sender.sendMessage("§bInvite link: " + invi);
@ -128,4 +135,12 @@ public class DiscordMCCommand extends ICommand2MC {
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.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 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() {
return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L);
return DPUtils.channelData(getConfig(), "logChannel");
}
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
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 logChannel.flatMap(ch -> ch.createMessage("Added " + role.getName() + " as game role. If you don't want this, change the role's color from the game role color."));
return Mono.empty();
}).subscribe();
}, 100);
@ -81,7 +88,7 @@ public class GameRoleModule extends Component<DiscordPlugin> {
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 logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the color of one."));
}
}
return Mono.empty();

View file

@ -11,7 +11,6 @@ import lombok.val;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.stream.Collectors;
@CommandClass
public class RoleCommand extends ICommand2DC {
@ -62,7 +61,20 @@ public class RoleCommand extends ICommand2DC {
@Command2.Subcommand
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) {

View file

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