@@ -1,5 +1,20 @@
package github.scarsz.discordsrv.util;

import com.google.common.io.CharStreams;
import com.google.gson.JsonObject;
import github.scarsz.discordsrv.DiscordSRV;
import net.dv8tion.jda.core.Permission;
import org.apache.commons.io.FileUtils;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;

import java.io.*;
import java.lang.management.ManagementFactory;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.*;

/**
* Made by Scarsz
*
@@ -9,9 +24,172 @@
*/
public class DebugUtil {

public static String run() {
//todo
return "";
public static String run(String requester) {
Map<String, String> files = new LinkedHashMap<String, String>() {{
try {
put("discordsrv-info.txt", String.join("\n", new String[]{
"Requested by " + requester,
"",
DiscordSRV.getPlugin().getRandomPhrase(),
"",
"plugin version: " + DiscordSRV.getPlugin() + " snapshot " + DiscordSRV.snapshotId,
"config version: " + DiscordSRV.getPlugin().getConfig().getString("ConfigVersion"),
"channels: " + DiscordSRV.getPlugin().getChannels(),
"console channel: " + DiscordSRV.getPlugin().getConsoleChannel(),
"main chat channel: " + DiscordSRV.getPlugin().getMainChatChannelPair(),
"unsubscribed players: " + DiscordSRV.getPlugin().getUnsubscribedPlayers(),
"colors: " + DiscordSRV.getPlugin().getColors(),
"threads:",
" channel topic updater -> alive: " + (DiscordSRV.getPlugin().getChannelTopicUpdater() != null && DiscordSRV.getPlugin().getChannelTopicUpdater().isAlive()),
" console message queue worker -> alive: " + (DiscordSRV.getPlugin().getConsoleMessageQueueWorker() != null && DiscordSRV.getPlugin().getConsoleMessageQueueWorker().isAlive()),
"hooked plugins: " + DiscordSRV.getPlugin().getHookedPlugins()
}));
put("relevant-lines-from-server.log", getRelevantLinesFromServerLog());
put("config.yml", FileUtils.readFileToString(DiscordSRV.getPlugin().getConfigFile(), Charset.defaultCharset())
.replace(DiscordSRV.getPlugin().getConfig().getString("BotToken"), "BOT-TOKEN-REDACTED"));
put("server-info.txt", getServerInfo());
put("channel-permissions.txt", getChannelPermissions());
put("system-info.txt", getSystemInfo());
} catch (IOException e) {
e.printStackTrace();
}
}};

return uploadToGists(files);
}

private static String getRelevantLinesFromServerLog() {
List<String> output = new LinkedList<>();
try {
FileReader fr = new FileReader(new File(new File("."), "logs/latest.log"));
BufferedReader br = new BufferedReader(fr);
boolean done = false;
while (!done) {
String line = br.readLine();
if (line == null) done = true;
if (line != null && line.toLowerCase().contains("discordsrv")) output.add(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return String.join("\n", output);
}

private static String getServerInfo() {
List<String> output = new LinkedList<>();

List<String> plugins = new LinkedList<>();
for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) plugins.add(plugin.toString());
Collections.sort(plugins);

output.add("server name: " + DiscordUtil.stripColor(Bukkit.getServerName()));
output.add("server motd: " + DiscordUtil.stripColor(Bukkit.getMotd()));
output.add("server players: " + Bukkit.getOnlinePlayers().size() + "/" + Bukkit.getMaxPlayers());
output.add("server plugins: " + plugins);
output.add("");
output.add("Minecraft version: " + Bukkit.getVersion());
output.add("Bukkit API version: " + Bukkit.getBukkitVersion());

return String.join("\n", output);
}

private static String getChannelPermissions() {
List<String> output = new LinkedList<>();
DiscordSRV.getPlugin().getChannels().forEach((ingameChannelName, textChannel) -> {
if (textChannel != null) {
List<String> outputForChannel = new LinkedList<>();
if (DiscordUtil.checkPermission(textChannel, Permission.MESSAGE_READ)) outputForChannel.add("read");
if (DiscordUtil.checkPermission(textChannel, Permission.MESSAGE_WRITE)) outputForChannel.add("write");
if (DiscordUtil.checkPermission(textChannel, Permission.MANAGE_CHANNEL)) outputForChannel.add("channel-manage");
if (DiscordUtil.checkPermission(textChannel, Permission.MESSAGE_MANAGE)) outputForChannel.add("message-manage");
output.add(textChannel + " (<- " + ingameChannelName + "): " + String.join(", ", outputForChannel));
}
});
return String.join("\n", output);
}

private static String getSystemInfo() {
List<String> output = new LinkedList<>();

// total number of processors or cores available to the JVM
output.add("Available processors (cores): " + Runtime.getRuntime().availableProcessors());
output.add("");

// memory
output.add("Free memory for JVM (MB): " + Runtime.getRuntime().freeMemory() / 1024 / 1024);
output.add("Maximum memory for JVM (MB): " + (Runtime.getRuntime().maxMemory() == Long.MAX_VALUE ? "no limit" : Runtime.getRuntime().maxMemory() / 1024 / 1024));
output.add("Total memory available for JVM (MB): " + Runtime.getRuntime().totalMemory() / 1024 / 1024);
output.add("");

// drive space
File[] roots = File.listRoots();
for (File root : roots) {
output.add("file system " + root.getAbsolutePath());
output.add("- total space (MB): " + root.getTotalSpace() / 1024 / 1024);
output.add("- free space (MB): " + root.getFreeSpace() / 1024 / 1024);
output.add("- usable space (MB): " + root.getUsableSpace() / 1024 / 1024);
}
output.add("");

// system properties
output.add("System properties:");
ManagementFactory.getRuntimeMXBean().getSystemProperties().forEach((key, value) -> output.add(key + ": " + value));

return String.join("\n", output);
}

/**
* Upload the given file map to GitHub Gists
* @param files A Map representing a structure of file name & it's contents
* @return A user-friendly message of how the report went
*/
private static String uploadToGists(Map<String, String> files) {
Map<String, String> newFilesMap = new LinkedHashMap<>();
files.forEach((fileName, fileContent) -> newFilesMap.put((newFilesMap.size() + 1) + "-" + fileName, fileContent));

HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) new URL("https://api.github.com/gists").openConnection();
connection.setRequestProperty("Content-Type", "application/json");
connection.addRequestProperty("User-Agent", "DiscordSRV v" + DiscordSRV.getPlugin().getDescription().getVersion());
connection.setRequestMethod("POST");
connection.setDoOutput(true);

OutputStream out = connection.getOutputStream();
JsonObject payload = new JsonObject();
payload.addProperty("description", "DiscordSRV Debug Report - Generated " + new Date());
payload.addProperty("public", "false");

JsonObject filesJson = new JsonObject();
newFilesMap.forEach((fileName, fileContent) -> {
JsonObject file = new JsonObject();
file.addProperty("content", fileContent);
filesJson.add(fileName, file);
});
payload.add("files", filesJson);

out.write(DiscordSRV.getPlugin().getGson().toJson(payload).getBytes(Charset.forName("UTF-8")));
out.close();

String rawOutput = CharStreams.toString(new InputStreamReader(connection.getInputStream()));
connection.getInputStream().close();
JsonObject output = DiscordSRV.getPlugin().getGson().fromJson(rawOutput, JsonObject.class);

if (!output.has("html_url")) throw new InvalidObjectException("HTML URL was not given in Gist output");

return "We've made a debug report with useful information, report your issue and provide this url: " + output.get("html_url").getAsString();
} catch (Exception e) {
try {
// check if 403 & due to rate limit being hit
if (connection != null && connection.getResponseCode() == 403 && connection.getHeaderField("X-RateLimit-Remaining").equals("0"))
return "Failed to send debug report: you may only create 60 dumps per hour, please try again in a bit.";
} catch (IOException e1) {
return "Failed to send debug report: failed to connect to GitHub Gists: " + e1.getLocalizedMessage();
}

e.printStackTrace();
return "Failed to send debug report: check the server console for further details";
}
}

}
@@ -13,6 +13,9 @@
import java.util.function.Consumer;
import java.util.regex.Pattern;

import static github.scarsz.discordsrv.DiscordSRV.debug;
import static github.scarsz.discordsrv.DiscordSRV.warning;

/**
* Made by Scarsz
*
@@ -124,16 +127,41 @@ public static void sendMessage(TextChannel channel, String message) {
* @param expiration milliseconds until expiration of message. if this is 0, the message will not expire
*/
public static void sendMessage(TextChannel channel, String message, int expiration) {
if (channel == null || getJda() == null || message == null || message.equals("") ||
!checkPermission(channel, Permission.MESSAGE_READ) ||
!checkPermission(channel, Permission.MESSAGE_WRITE))
if (channel == null) {
debug("Tried sending a message to a null channel");
return;
}

if (getJda() == null) {
debug("Tried sending a message using a null JDA instance");
return;
}

if (!checkPermission(channel, Permission.MESSAGE_READ)) {
debug("Tried sending a message to channel " + channel + " but the bot doesn't have read permissions for that channel");
return;
}

if (!checkPermission(channel, Permission.MESSAGE_WRITE)) {
debug("Tried sending a message to channel " + channel + " but the bot doesn't have write permissions for that channel");
return;
}

if (message == null) {
debug("Tried sending a null message to " + channel);
return;
}

if (message.equals("")) {
debug("Tried sending a blank message to " + channel);
return;
}

message = DiscordUtil.stripColor(message);

String overflow = null;
if (message.length() > 2000) {
DiscordSRV.warning("Tried sending message with length of " + message.length() + " (" + (message.length() - 2000) + " over limit)");
warning("Tried sending message with length of " + message.length() + " (" + (message.length() - 2000) + " over limit)");
overflow = message.substring(2000);
message = message.substring(0, 2000);
}
@@ -154,7 +182,7 @@ public static void sendMessage(TextChannel channel, String message, int expirati
* @return true if the permission is obtained, false otherwise
*/
public static boolean checkPermission(Channel channel, Permission permission) {
return checkPermission(channel, channel.getJDA().getSelfUser(), permission);
return checkPermission(channel, getJda().getSelfUser(), permission);
}
/**
* Check if the given user has the given permission in the given channel
@@ -164,6 +192,7 @@ public static boolean checkPermission(Channel channel, Permission permission) {
* @return true if the permission is obtained, false otherwise
*/
public static boolean checkPermission(Channel channel, User user, Permission permission) {
if (channel == null) return false;
return channel.getGuild().getMember(user).hasPermission(channel, permission);
}

@@ -183,6 +212,20 @@ public static Message sendMessageBlocking(TextChannel channel, String message) {
* @return The sent message
*/
public static Message sendMessageBlocking(TextChannel channel, Message message) {
if (channel == null) {
debug("Tried sending a message to a null channel");
return null;
}

if (!DiscordUtil.checkPermission(channel, Permission.MESSAGE_READ)) {
debug("Tried sending a message to channel " + channel + " of which the bot doesn't have read permission for");
return null;
}
if (!DiscordUtil.checkPermission(channel, Permission.MESSAGE_WRITE)) {
debug("Tried sending a message to channel " + channel + " of which the bot doesn't have write permission for");
return null;
}

try {
return channel.sendMessage(message).block();
} catch (RateLimitedException e) {
@@ -224,16 +267,16 @@ public static void queueMessage(TextChannel channel, String message, Consumer<Me
*/
public static void queueMessage(TextChannel channel, Message message, Consumer<Message> consumer) {
if (channel == null) {
DiscordSRV.debug("Tried sending a message to a null channel");
debug("Tried sending a message to a null channel");
return;
}

if (!DiscordUtil.checkPermission(channel, Permission.MESSAGE_READ)) {
DiscordSRV.debug("Tried sending a message to channel " + channel + " of which the bot doesn't have read permission for");
debug("Tried sending a message to channel " + channel + " of which the bot doesn't have read permission for");
return;
}
if (!DiscordUtil.checkPermission(channel, Permission.MESSAGE_WRITE)) {
DiscordSRV.debug("Tried sending a message to channel " + channel + " of which the bot doesn't have write permission for");
debug("Tried sending a message to channel " + channel + " of which the bot doesn't have write permission for");
return;
}

@@ -249,12 +292,12 @@ public static void queueMessage(TextChannel channel, Message message, Consumer<M
*/
public static void setTextChannelTopic(TextChannel channel, String topic) {
if (channel == null) {
DiscordSRV.debug("Attempted to set status of null channel");
debug("Attempted to set status of null channel");
return;
}

if (!DiscordUtil.checkPermission(channel, Permission.MANAGE_CHANNEL)) {
DiscordSRV.warning("Unable to update topic of " + channel + " because the bot is missing the \"Manage Channel\" permission. Did you follow the instructions?");
warning("Unable to update topic of " + channel + " because the bot is missing the \"Manage Channel\" permission. Did you follow the instructions?");
return;
}

@@ -269,24 +312,39 @@ public static void setTextChannelTopic(TextChannel channel, String topic) {
*/
public static void setGameStatus(String gameStatus) {
if (getJda() == null) {
DiscordSRV.debug("Attempted to set game status using null JDA");
debug("Attempted to set game status using null JDA");
return;
}
if (gameStatus == null || gameStatus.isEmpty()) {
DiscordSRV.debug("Attempted setting game status to a null or empty string");
debug("Attempted setting game status to a null or empty string");
return;
}

getJda().getPresence().setGame(Game.of(gameStatus));
}

/**
* Delete the given message, given the bot has permission to
* @param message The message to delete
*/
public static void deleteMessage(Message message) {
if (message.isFromType(ChannelType.PRIVATE)) return;

if (!checkPermission(message.getTextChannel(), Permission.MESSAGE_MANAGE)) {
DiscordSRV.warning("Could not delete message in channel " + message.getTextChannel() + ", no permission to manage messages");
warning("Could not delete message in channel " + message.getTextChannel() + ", no permission to manage messages");
return;
}

message.deleteMessage().queue();
}

/**
* Open the private channel for the given user and send them the given message
* @param user User to send the message to
* @param message Message to send to the user
*/
public static void privateMessage(User user, String message) {
user.openPrivateChannel().queue(privateChannel -> privateChannel.sendMessage(message).queue());
}

}

This file was deleted.

@@ -18,11 +18,6 @@ Debug: false
CancelConsoleCommandIfLoggingFailed: true
ColorLookupDebug: false
DontSendCanceledChatEvents: true
EventDebug: false
LegacyConsoleChannelEngine: false
PlayerVanishLookupReporting: false
ReportCanceledChatEvents: false
UseOldConsoleCommandSender: false

# Hooks and the format for the message
#
@@ -49,13 +44,37 @@ DiscordGameStatus: "Minecraft"
# DiscordChatChannelPrefix: the character(s) required to prefix a message for it to be sent from Minecraft to Discord (example "!")
# DiscordChatChannelRolesAllowedToUseColorCodesInChat: list of roles allowed to use color/format codes in Discord to Minecraft chat
# DiscordChatChannelBroadcastDiscordMessagesToConsole: whether or not to print processed discord messages to the console
# DiscordChatChannelColorTranslations: hex representations of Discord roles to be matched with for showing role colors in-game with their in-game equivalent
#
DiscordChatChannelDiscordToMinecraft: true
DiscordChatChannelMinecraftToDiscord: true
DiscordChatChannelTruncateLength: 100
DiscordChatChannelPrefix: ""
DiscordChatChannelRolesAllowedToUseColorCodesInChat: ["Developer", "Owner", "Admin", "Moderator"]
DiscordChatChannelBroadcastDiscordMessagesToConsole: true
DiscordChatChannelColorTranslations: {
"99AAB5": "&f",
"1ABC9C": "&a",
"2ECC71": "&a",
"3498DB": "&3",
"9B59B6": "&5",
"E91E63": "&d",
"F1C40F": "&e",
"E67E22": "&6",
"E74C3C": "&c",
"95A5A6": "&7",
"607D8B": "&8",
"11806A": "&2",
"1F8B4C": "&2",
"206694": "&1",
"71368A": "&5",
"AD1457": "&d",
"C27C0E": "&6",
"A84300": "&6",
"992D22": "&4",
"979C9F": "&7",
"546E7A": "&8"
}

# Console channel information
# The console channel is the text channel that receives messages which are then run as server commands
@@ -267,6 +286,6 @@ DiscordCannedResponses: {"!ip": "yourserveripchange.me", "!site": "http://yoursi
# %minecraftplayername% = player's Minecraft username
# %minecraftuuid% = player's uuid
# %discordid% = linked discord account's id
# %discordname% = linked discord account's name
# %discordname% = linked discord account's username
#
MinecraftDiscordAccountLinkedConsoleCommand: ""