├── settings.gradle ├── libs └── javacord-2.0.14-shaded.jar ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src └── main │ └── java │ └── com │ └── nguyenquyhy │ └── discordbridge │ ├── models │ ├── TokenStore.java │ ├── IConfigInheritable.java │ ├── ChannelType.java │ ├── SpongeChannelConfig.java │ ├── ChannelMinecraftConfig.java │ ├── ChannelConfig.java │ ├── ChannelMinecraftEmojiConfig.java │ ├── ChannelMinecraftAttachmentConfig.java │ ├── GlobalConfig.java │ ├── ChannelMinecraftMentionConfig.java │ ├── ChannelMinecraftConfigCore.java │ └── ChannelDiscordConfig.java │ ├── database │ ├── IStorage.java │ ├── InMemoryStorage.java │ └── JsonFileStorage.java │ ├── utils │ ├── ChannelUtil.java │ ├── ConfigUtil.java │ ├── ErrorMessages.java │ ├── Emoji.java │ ├── DiscordUtil.java │ ├── ColorUtil.java │ └── TextUtil.java │ ├── commands │ ├── OtpCommand.java │ ├── LogoutCommand.java │ ├── LoginConfirmCommand.java │ ├── LoginCommand.java │ ├── ReloadCommand.java │ ├── StatusCommand.java │ ├── ReconnectCommand.java │ └── BroadcastCommand.java │ ├── listeners │ ├── DeathListener.java │ ├── ClientConnectionListener.java │ └── ChatListener.java │ ├── CommandRegistry.java │ ├── logics │ ├── MessageHandler.java │ ├── ConfigHandler.java │ └── LoginHandler.java │ └── DiscordBridge.java ├── MIGRATE.md ├── LICENSE.md ├── examples ├── config-simple.json └── config-multiple.json ├── EMOJI.md ├── GETTING STARTED.md ├── gradlew.bat ├── CHANGELOG.md ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'DiscordBridge' 2 | 3 | -------------------------------------------------------------------------------- /libs/javacord-2.0.14-shaded.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenquyhy/DiscordBridge/HEAD/libs/javacord-2.0.14-shaded.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenquyhy/DiscordBridge/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /.classpath 4 | /.project 5 | /build 6 | /classes 7 | /run 8 | /*.iml 9 | /bin/ 10 | /.settings/ -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/TokenStore.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | /** 4 | * Created by Hy on 10/13/2016. 5 | */ 6 | public enum TokenStore { 7 | NONE, MEMORY, JSON 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/IConfigInheritable.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | /** 4 | * Created by Hy on 12/11/2016. 5 | */ 6 | public interface IConfigInheritable { 7 | void inherit(T parent); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelType.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | /** 4 | * Created by Hy on 10/13/2016. 5 | */ 6 | public enum ChannelType { 7 | BIDIRECTION, 8 | TO_DISCORD, 9 | TO_MINECRAFT 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 06 14:03:12 SGT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip 7 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/database/IStorage.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.database; 2 | 3 | import java.io.IOException; 4 | import java.util.UUID; 5 | 6 | /** 7 | * Created by Hy on 1/5/2016. 8 | */ 9 | public interface IStorage { 10 | void putToken(UUID player, String token) throws IOException; 11 | 12 | String getToken(UUID player); 13 | 14 | void removeToken(UUID player) throws IOException; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/SpongeChannelConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 10/29/2016. 8 | */ 9 | @ConfigSerializable 10 | public class SpongeChannelConfig { 11 | @Setting 12 | public String anonymousChatTemplate; 13 | @Setting 14 | public String authenticatedChatTemplate; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/ChannelUtil.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import net.dv8tion.jda.core.entities.TextChannel; 4 | 5 | import java.util.Random; 6 | 7 | /** 8 | * Created by Hy on 12/4/2016. 9 | */ 10 | public class ChannelUtil { 11 | public static final String SPECIAL_CHAR = "\u2062"; 12 | public static final String BOT_RANDOM = String.valueOf(new Random().nextInt(100000)); 13 | 14 | public static void sendMessage(TextChannel channel, String content) { 15 | channel.sendMessage(content).nonce(SPECIAL_CHAR + BOT_RANDOM).queue(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/OtpCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 4 | import org.spongepowered.api.command.CommandException; 5 | import org.spongepowered.api.command.CommandResult; 6 | import org.spongepowered.api.command.CommandSource; 7 | import org.spongepowered.api.command.args.CommandContext; 8 | import org.spongepowered.api.command.spec.CommandExecutor; 9 | 10 | public class OtpCommand implements CommandExecutor { 11 | @Override 12 | public CommandResult execute(CommandSource src, CommandContext args) throws CommandException { 13 | return LoginHandler.otp(src, args.getOne("code").get()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MIGRATE.md: -------------------------------------------------------------------------------- 1 | ## Migrating from version 1.x.x to latest 2 | - `/discord default` command and default account are no longer available. You must setup a Discord Bot for the plugin to properly function. 3 | - You have to manually rename config folder `com.nguyenquyhy.spongediscord` into `discordbridge` due to plugin ID change. 4 | - Your current configuration will be migrated automatically from `config.conf` into `config.json`. 5 | - Invite code has been removed. Please contact me if you have specific need for that. 6 | - Default anonymous chat template is changed to ```"`%a:\` %s"```, which looks nicer in my opinion. 7 | 8 | ## Migrating from version 2.0.0 to latest 9 | - You have to manually rename config folder `com.nguyenquyhy.spongediscord` into `discordbridge` due to plugin ID change. -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/LogoutCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 4 | import org.spongepowered.api.command.CommandException; 5 | import org.spongepowered.api.command.CommandResult; 6 | import org.spongepowered.api.command.CommandSource; 7 | import org.spongepowered.api.command.args.CommandContext; 8 | import org.spongepowered.api.command.spec.CommandExecutor; 9 | 10 | /** 11 | * Created by Hy on 1/4/2016. 12 | */ 13 | public class LogoutCommand implements CommandExecutor { 14 | @Override 15 | public CommandResult execute(CommandSource commandSource, CommandContext commandContext) throws CommandException { 16 | return LoginHandler.logout(commandSource, false); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelMinecraftConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by Hy on 10/13/2016. 11 | */ 12 | @ConfigSerializable 13 | public class ChannelMinecraftConfig extends ChannelMinecraftConfigCore { 14 | public ChannelMinecraftConfig() { 15 | initializeDefault(); 16 | } 17 | 18 | @Override 19 | void initializeDefault() { 20 | super.initializeDefault(); 21 | roles = new HashMap<>(); 22 | } 23 | 24 | @Setting 25 | public Map roles; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/database/InMemoryStorage.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.database; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.UUID; 6 | 7 | /** 8 | * Created by Hy on 1/5/2016. 9 | */ 10 | public class InMemoryStorage implements IStorage { 11 | private final Map tokens = new HashMap<>(); 12 | 13 | @Override 14 | public void putToken(UUID player, String token) { 15 | tokens.put(player, token); 16 | } 17 | 18 | @Override 19 | public String getToken(UUID player) { 20 | if (tokens.containsKey(player)) 21 | return tokens.get(player); 22 | else 23 | return null; 24 | } 25 | 26 | @Override 27 | public void removeToken(UUID player) { 28 | tokens.remove(player); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/ConfigUtil.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import ninja.leaping.configurate.ConfigurationNode; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * Created by Hy on 5/31/2016. 8 | */ 9 | public class ConfigUtil { 10 | public static String readString(ConfigurationNode node, String name, String defaultValue) { 11 | if (node.getNode(name).getValue() == null) 12 | node.getNode(name).setValue(defaultValue); 13 | return node.getNode(name).getString(); 14 | } 15 | 16 | /** 17 | * @param config the config value to be read 18 | * @param defaultValue the value to use if config is null or empty 19 | * @return non-null non-empty config value or default 20 | */ 21 | public static String get(String config, String defaultValue) { 22 | return (StringUtils.isBlank(config)) ? defaultValue : config; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/ErrorMessages.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import org.slf4j.Logger; 5 | 6 | /** 7 | * Created by Hy on 11/7/2016. 8 | */ 9 | public enum ErrorMessages { 10 | CHANNEL_NOT_FOUND, 11 | CHANNEL_NOT_FOUND_HUMAN, 12 | BOT_TOKEN_NOT_FOUND; 13 | 14 | 15 | @SuppressWarnings("incomplete-switch") 16 | public void log(String... params) { 17 | Logger logger = DiscordBridge.getInstance().getLogger(); 18 | switch (this) { 19 | case CHANNEL_NOT_FOUND: 20 | logger.error("Channel ID " + params[0] + " cannot be found! Please make sure the channel ID is correct and the bot has read & write permission for the channel."); 21 | return; 22 | case CHANNEL_NOT_FOUND_HUMAN: 23 | logger.error("Channel ID " + params[0] + " cannot be found! Please make sure the channel ID is correct and the user has read & write permission for the channel."); 24 | return; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nguyen Quy Hy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/LoginConfirmCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 4 | import org.spongepowered.api.command.CommandException; 5 | import org.spongepowered.api.command.CommandResult; 6 | import org.spongepowered.api.command.CommandSource; 7 | import org.spongepowered.api.command.args.CommandContext; 8 | import org.spongepowered.api.command.spec.CommandExecutor; 9 | import org.spongepowered.api.text.Text; 10 | import org.spongepowered.api.text.format.TextColors; 11 | 12 | /** 13 | * Created by Hy on 1/4/2016. 14 | */ 15 | public class LoginConfirmCommand implements CommandExecutor { 16 | @Override 17 | public CommandResult execute(CommandSource commandSource, CommandContext commandContext) throws CommandException { 18 | String email = commandContext.getOne("email").get(); 19 | String password = commandContext.getOne("password").get(); 20 | 21 | // Sign in to Discord 22 | commandSource.sendMessage(Text.of(TextColors.GRAY, "Logging in to Discord...")); 23 | return LoginHandler.login(commandSource, email, password); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 10/13/2016. 8 | */ 9 | @ConfigSerializable 10 | public class ChannelConfig { 11 | /** 12 | * Configs initialized in constructor will be restored automatically if deleted. 13 | */ 14 | public ChannelConfig() { 15 | discordId = "DISCORD_CHANNEL_ID"; 16 | } 17 | 18 | /** 19 | * This is called only when the config file is first created. 20 | */ 21 | public void initializeDefault() { 22 | discord = new ChannelDiscordConfig(); 23 | discord.initializeDefault(); 24 | minecraft = new ChannelMinecraftConfig(); 25 | minecraft.initializeDefault(); 26 | } 27 | 28 | @Setting 29 | public String discordId; 30 | @Setting 31 | public ChannelDiscordConfig discord; 32 | @Setting 33 | public ChannelMinecraftConfig minecraft; 34 | 35 | public void migrate() { 36 | if (discord != null) 37 | discord.migrate(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelMinecraftEmojiConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 12/11/2016. 8 | */ 9 | @ConfigSerializable 10 | public class ChannelMinecraftEmojiConfig implements IConfigInheritable { 11 | public ChannelMinecraftEmojiConfig() { 12 | 13 | } 14 | 15 | /** 16 | * This is called only when the config file is first created. 17 | */ 18 | void initializeDefault() { 19 | template = "&b:%n:&r"; 20 | hoverTemplate = "Click to view emjoi."; 21 | allowLink = true; 22 | } 23 | 24 | @Setting 25 | public String template; 26 | @Setting 27 | public String hoverTemplate; 28 | @Setting 29 | public Boolean allowLink; 30 | 31 | @Override 32 | public void inherit(ChannelMinecraftEmojiConfig parent) { 33 | if (template == null) template = parent.template; 34 | if (hoverTemplate == null) hoverTemplate = parent.hoverTemplate; 35 | if (allowLink == null) allowLink = parent.allowLink; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelMinecraftAttachmentConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 12/11/2016. 8 | */ 9 | @ConfigSerializable 10 | public class ChannelMinecraftAttachmentConfig implements IConfigInheritable { 11 | public ChannelMinecraftAttachmentConfig() { 12 | 13 | } 14 | 15 | /** 16 | * This is called only when the config file is first created. 17 | */ 18 | void initializeDefault() { 19 | template = "&3[Attachment]&r"; 20 | hoverTemplate = "Click to open attachment."; 21 | allowLink = true; 22 | } 23 | 24 | @Setting 25 | public String template; 26 | @Setting 27 | public String hoverTemplate; 28 | @Setting 29 | public Boolean allowLink; 30 | 31 | @Override 32 | public void inherit(ChannelMinecraftAttachmentConfig parent) { 33 | if (template == null) template = parent.template; 34 | if (hoverTemplate == null) hoverTemplate = parent.hoverTemplate; 35 | if (allowLink == null) allowLink = parent.allowLink; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/LoginCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import org.spongepowered.api.command.CommandException; 4 | import org.spongepowered.api.command.CommandResult; 5 | import org.spongepowered.api.command.CommandSource; 6 | import org.spongepowered.api.command.args.CommandContext; 7 | import org.spongepowered.api.command.spec.CommandExecutor; 8 | import org.spongepowered.api.text.Text; 9 | import org.spongepowered.api.text.format.TextColors; 10 | import org.spongepowered.api.text.format.TextStyles; 11 | 12 | /** 13 | * Created by Hy on 10/15/2016. 14 | */ 15 | public class LoginCommand implements CommandExecutor { 16 | @Override 17 | public CommandResult execute(CommandSource src, CommandContext args) throws CommandException { 18 | src.sendMessage(Text.of(TextColors.RED, TextStyles.BOLD, "WARNING!!")); 19 | src.sendMessage(Text.of(TextColors.RED, "You will need to provide Discord email & password to login.")); 20 | src.sendMessage(Text.of(TextColors.RED, "This server might record those credential in its log!")); 21 | src.sendMessage(Text.of(TextColors.RED, "Proceed only if you completely trust the server owners/staffs with those information!")); 22 | src.sendMessage(Text.of(TextColors.GRAY, "To proceed, use: /discord loginconfirm ")); 23 | return CommandResult.success(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/GlobalConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * Created by Hy on 10/13/2016. 11 | */ 12 | @ConfigSerializable 13 | public class GlobalConfig { 14 | /** 15 | * Configs initialized in constructor will be restored automatically if deleted. 16 | */ 17 | public GlobalConfig() { 18 | channels = new ArrayList<>(); 19 | tokenStore = TokenStore.JSON; 20 | prefixBlacklist = new ArrayList<>(); 21 | ignoreBots = false; 22 | botDiscordGame = ""; 23 | minecraftBroadcastTemplate = "&2 %s"; 24 | botToken = ""; 25 | } 26 | 27 | @Setting 28 | public String botToken; 29 | @Setting 30 | public TokenStore tokenStore; 31 | @Setting 32 | public List prefixBlacklist; 33 | @Setting 34 | public Boolean ignoreBots; 35 | @Setting 36 | public String botDiscordGame; 37 | @Setting 38 | public String minecraftBroadcastTemplate; 39 | @Setting 40 | public List channels; 41 | 42 | public void migrate() { 43 | if (channels != null) { 44 | channels.forEach(ChannelConfig::migrate); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelMinecraftMentionConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 12/11/2016. 8 | */ 9 | @ConfigSerializable 10 | public class ChannelMinecraftMentionConfig implements IConfigInheritable { 11 | public ChannelMinecraftMentionConfig() { 12 | 13 | } 14 | 15 | /** 16 | * This is called only when the config file is first created. 17 | */ 18 | void initializeDefault() { 19 | userTemplate = "@%s"; 20 | roleTemplate = "@%s"; 21 | everyoneTemplate = "&6@%s"; 22 | channelTemplate = "&9#%s"; 23 | } 24 | 25 | @Setting 26 | public String userTemplate; 27 | @Setting 28 | public String roleTemplate; 29 | @Setting 30 | public String everyoneTemplate; 31 | @Setting 32 | public String channelTemplate; 33 | 34 | @Override 35 | public void inherit(ChannelMinecraftMentionConfig parent) { 36 | if (userTemplate == null) userTemplate = parent.userTemplate; 37 | if (roleTemplate == null) roleTemplate = parent.roleTemplate; 38 | if (everyoneTemplate == null) everyoneTemplate = parent.everyoneTemplate; 39 | if (channelTemplate == null) channelTemplate = parent.channelTemplate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/ReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.logics.ConfigHandler; 5 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 6 | import org.slf4j.Logger; 7 | import org.spongepowered.api.command.CommandException; 8 | import org.spongepowered.api.command.CommandResult; 9 | import org.spongepowered.api.command.CommandSource; 10 | import org.spongepowered.api.command.args.CommandContext; 11 | import org.spongepowered.api.command.spec.CommandExecutor; 12 | import org.spongepowered.api.text.Text; 13 | 14 | /** 15 | * Created by Hy on 1/5/2016. 16 | */ 17 | public class ReloadCommand implements CommandExecutor { 18 | @Override 19 | public CommandResult execute(CommandSource commandSource, CommandContext commandContext) throws CommandException { 20 | Logger logger = DiscordBridge.getInstance().getLogger(); 21 | try { 22 | GlobalConfig config = ConfigHandler.loadConfiguration(); 23 | DiscordBridge.getInstance().setConfig(config); 24 | logger.info("Configuration reloaded!"); 25 | commandSource.sendMessage(Text.of("Configuration reloaded!")); 26 | 27 | return CommandResult.success(); 28 | } catch (Exception e) { 29 | logger.error("Cannot reload configuration!", e); 30 | return CommandResult.empty(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/Emoji.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | /** 4 | * Created by Hy on 8/29/2016. 5 | */ 6 | public enum Emoji { 7 | Smiley(":smiley:", ":)", "\ud83d\ude03"), 8 | Smile(":smile:", ":D", "\ud83d\ude04"), 9 | Joy(":joy:", ";D", "\ud83d\ude02"), 10 | Laughing(":laughing:", "xD", "\ud83d\ude06"), 11 | Frowning(":frowning:", ":(", "\ud83d\ude26"), 12 | Sob(":sob:", ";(", "\ud83d\ude2d"), 13 | TiredFace(":tired_face:", "x(", "\ud83d\ude2b"), 14 | Wink(":wink:", ";)", "\ud83d\ude09"), 15 | StuckOutTongue(":stuck_out_tongue:", ":P", "\ud83d\ude1b"), 16 | StuckOutTongueWinkingEye(":stuck_out_tongue_winking_eye:", ";P", "\ud83d\ude1c"), 17 | StuckOutTongueClosedEyes(":stuck_out_tongue_closed_eyes:", "xP", "\ud83d\ude1d"), 18 | OpenMouth(":open_mouth:", ":O", "\ud83d\ude2e"), 19 | DizzyFace(":dizzy_face:", "xO", "\ud83d\ude35"), 20 | NeutralFace(":neutral_face:", ":|", "\ud83d\ude10"), 21 | Sunglasses(":sunglasses:", "B)", "\ud83d\ude0e"), 22 | Kissing(":kissing:", ":*", "\ud83d\ude17"), 23 | Heart(":heart:", "<3", "\u2764"); 24 | 25 | public final String discordFormat; 26 | public final String minecraftFormat; 27 | public final String unicode; 28 | 29 | Emoji(String discordFormat, String minecraftFormat, String unicode) { 30 | this.discordFormat = discordFormat; 31 | this.minecraftFormat = minecraftFormat; 32 | this.unicode = unicode; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/StatusCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import net.dv8tion.jda.core.JDA; 5 | import org.spongepowered.api.command.CommandException; 6 | import org.spongepowered.api.command.CommandResult; 7 | import org.spongepowered.api.command.CommandSource; 8 | import org.spongepowered.api.command.args.CommandContext; 9 | import org.spongepowered.api.command.spec.CommandExecutor; 10 | import org.spongepowered.api.text.Text; 11 | import org.spongepowered.api.text.format.TextColors; 12 | 13 | import java.util.concurrent.ExecutionException; 14 | 15 | /** 16 | * Created by Hy on 10/15/2016. 17 | */ 18 | public class StatusCommand implements CommandExecutor { 19 | @Override 20 | public CommandResult execute(CommandSource src, CommandContext args) throws CommandException { 21 | DiscordBridge mod = DiscordBridge.getInstance(); 22 | JDA bot = mod.getBotClient(); 23 | 24 | boolean isProfileReady = false; 25 | if (bot != null) { 26 | isProfileReady = bot.getSelfUser() != null; 27 | } 28 | 29 | src.sendMessage(Text.of(TextColors.GREEN, "Bot account:")); 30 | src.sendMessage(Text.of("- Profile: " + (isProfileReady ? bot.getSelfUser().getName() : "Not available"))); 31 | src.sendMessage(Text.of("- Status: " + (bot == null ? "N/A" : bot.getStatus().toString()))); 32 | return CommandResult.success(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/config-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokenStore": "JSON", 3 | "botToken": "APP_BOT_USER_TOKEN", 4 | "botDiscordGame": "Minecraft", 5 | "prefixBlacklist": ["!"], 6 | "ignoreBots": false, 7 | "minecraftBroadcastTemplate": "&2 %s", 8 | "channels": [ 9 | { 10 | "discordId": "DISCORD_PUBLIC_CHANNEL_ID", 11 | "discordInviteCode": "DISCORD_INVITATION_CODE", 12 | "discord": { 13 | "publicChat": { 14 | "authenticatedChatTemplate": "%s", 15 | "anonymousChatTemplate": "`%a:` %s" 16 | }, 17 | "joinedTemplate": "_%s just joined the server_", 18 | "leftTemplate": "_%s just left the server_", 19 | "serverUpMessage": "Server has started.", 20 | "serverDownMessage": "Server has stopped.", 21 | "broadcastTemplate": "_ %s_", 22 | "deathTemplate": "**%s**" 23 | }, 24 | "minecraft": { 25 | "chatTemplate": "&7<%a> &f%s", 26 | "attachment": { 27 | "template": "&3[Attachment]", 28 | "hoverTemplate": "Click to open attachment.", 29 | "allowLink": true 30 | }, 31 | "emoji": { 32 | "template": "&b:%n:&r", 33 | "hoverTemplate": "Click to view emjoi.", 34 | "allowLink": true 35 | }, 36 | "mention": { 37 | "userTemplate": "@%s", 38 | "roleTemplate": "@%s", 39 | "everyoneTemplate": "&6@%s", 40 | "channelTemplate": "&9#%s" 41 | } 42 | } 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelMinecraftConfigCore.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | 6 | /** 7 | * Created by Hy on 12/11/2016. 8 | */ 9 | @ConfigSerializable 10 | public class ChannelMinecraftConfigCore implements IConfigInheritable { 11 | void initializeDefault() { 12 | chatTemplate = "&7<%a> &f%s"; 13 | attachment = new ChannelMinecraftAttachmentConfig(); 14 | attachment.initializeDefault(); 15 | emoji = new ChannelMinecraftEmojiConfig(); 16 | emoji.initializeDefault(); 17 | mention = new ChannelMinecraftMentionConfig(); 18 | mention.initializeDefault(); 19 | } 20 | 21 | @Setting 22 | public String chatTemplate; 23 | @Setting 24 | public ChannelMinecraftAttachmentConfig attachment; 25 | @Setting 26 | public ChannelMinecraftEmojiConfig emoji; 27 | @Setting 28 | public ChannelMinecraftMentionConfig mention; 29 | 30 | @Override 31 | public void inherit(ChannelMinecraftConfigCore parent) { 32 | if (chatTemplate == null) chatTemplate = parent.chatTemplate; 33 | if (attachment == null) attachment = parent.attachment; 34 | else attachment.inherit(parent.attachment); 35 | if (mention == null) mention = parent.mention; 36 | else mention.inherit(parent.mention); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/ReconnectCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.logics.ConfigHandler; 5 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 6 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 7 | import org.slf4j.Logger; 8 | import org.spongepowered.api.Sponge; 9 | import org.spongepowered.api.command.CommandException; 10 | import org.spongepowered.api.command.CommandResult; 11 | import org.spongepowered.api.command.CommandSource; 12 | import org.spongepowered.api.command.args.CommandContext; 13 | import org.spongepowered.api.command.spec.CommandExecutor; 14 | import org.spongepowered.api.entity.living.player.Player; 15 | 16 | import java.util.Optional; 17 | import java.util.UUID; 18 | 19 | /** 20 | * Created by Hy on 1/5/2016. 21 | */ 22 | public class ReconnectCommand implements CommandExecutor { 23 | @Override 24 | public CommandResult execute(CommandSource commandSource, CommandContext commandContext) throws CommandException { 25 | Logger logger = DiscordBridge.getInstance().getLogger(); 26 | try { 27 | GlobalConfig config = ConfigHandler.loadConfiguration(); 28 | DiscordBridge.getInstance().setConfig(config); 29 | logger.info("Configuration reloaded!"); 30 | 31 | LoginHandler.loginBotAccount(); 32 | for (UUID uuid : DiscordBridge.getInstance().getHumanClients().keySet()) { 33 | Optional player = Sponge.getServer().getPlayer(uuid); 34 | player.ifPresent(LoginHandler::loginHumanAccount); 35 | } 36 | 37 | return CommandResult.success(); 38 | } catch (Exception e) { 39 | logger.error("Cannot reload configuration!", e); 40 | return CommandResult.empty(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/listeners/DeathListener.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.listeners; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 5 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 6 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 7 | import com.nguyenquyhy.discordbridge.utils.ConfigUtil; 8 | import net.dv8tion.jda.core.JDA; 9 | import net.dv8tion.jda.core.entities.TextChannel; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.spongepowered.api.entity.living.player.Player; 12 | import org.spongepowered.api.event.Listener; 13 | import org.spongepowered.api.event.entity.DestructEntityEvent; 14 | 15 | public class DeathListener { 16 | @Listener 17 | public void onPlayerDeath(DestructEntityEvent.Death event) { 18 | DiscordBridge mod = DiscordBridge.getInstance(); 19 | GlobalConfig config = mod.getConfig(); 20 | JDA client = mod.getBotClient(); 21 | 22 | if (!(event.getTargetEntity() instanceof Player) || event.isMessageCancelled() || StringUtils.isBlank(event.getMessage().toPlain())) return; 23 | Player player = (Player) event.getTargetEntity(); 24 | 25 | if (client != null) { 26 | for (ChannelConfig channelConfig : config.channels) { 27 | if (StringUtils.isNotBlank(channelConfig.discordId) && channelConfig.discord != null) { 28 | String template = ConfigUtil.get(channelConfig.discord.deathTemplate, null); 29 | if (StringUtils.isNotBlank(template)) { 30 | TextChannel channel = client.getTextChannelById(channelConfig.discordId); 31 | ChannelUtil.sendMessage(channel, template.replace("%s", event.getMessage().toPlain())); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/DiscordUtil.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import net.dv8tion.jda.core.entities.*; 4 | 5 | import java.util.Optional; 6 | 7 | public class DiscordUtil { 8 | 9 | /** 10 | * @param name The name to search the server for valid a User 11 | * @param server The server to search through Users 12 | * @return The User, if any, that matches the name supplied 13 | */ 14 | static Optional getMemberByName(String name, Guild server) { 15 | for (Member member : server.getMembers()) { 16 | if (member.getEffectiveName().equalsIgnoreCase(name) || (member.getNickname() != null && member.getNickname().equalsIgnoreCase(name))) { 17 | return Optional.of(member); 18 | } 19 | } 20 | return Optional.empty(); 21 | } 22 | 23 | /** 24 | * @param name The name to search the server for valid a Role 25 | * @param server The server to search through Roles 26 | * @return The Role, if any, that matches the name supplied 27 | */ 28 | static Optional getRoleByName(String name, Guild server) { 29 | for (Role role : server.getRoles()) { 30 | if (role.getName().equalsIgnoreCase(name)) { 31 | return Optional.of(role); 32 | } 33 | } 34 | return Optional.empty(); 35 | } 36 | 37 | /** 38 | * @param name The name to search the server for valid a Channel 39 | * @param server The server to search through Roles 40 | * @return The Channel, if any, that matches the name supplied 41 | */ 42 | static Optional getChannelByName(String name, Guild server) { 43 | for (Channel channel : server.getTextChannels()) { 44 | if (channel.getName().equalsIgnoreCase(name)) { 45 | return Optional.of(channel); 46 | } 47 | } 48 | return Optional.empty(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /EMOJI.md: -------------------------------------------------------------------------------- 1 | # EMOJI 2 | 3 | This is a list of mapping that this plugin attempts to perform to make Emoji shows up better on Discord and Minecraft. 4 | 5 | If you have any request for more mappings or any modifications, please file a GitHub issue in this repository. You can check more Emojis at http://www.emoji-cheat-sheet.com. 6 | 7 | ## From Minecraft to Discord 8 | 9 | Minecraft | Discord | Discord Text 10 | ----------|---------|------------- 11 | `:)` | :smiley: | `:smiley:` 12 | `:D` or `:d` | :smile: | `:smile:` 13 | `;D` or `;d` | :joy: | `:joy:` 14 | `xD` or `XD` | :laughing: | `:laughing:` 15 | `:(` | :frowning: | `:frowning:` 16 | `;(` | :sob: | `:sob:` 17 | `x(` or `X(` | :tired_face: | `:tired_face:` 18 | `;)` | :wink: | `:wink:` 19 | `:P` or `:p` | :stuck_out_tongue: | `:stuck_out_tongue:` 20 | `;P` or `;p` | :stuck_out_tongue_winking_eye: | `:stuck_out_tongue_winking_eye:` 21 | `xP` | :stuck_out_tongue_closed_eyes: | `:stuck_out_tongue_closed_eyes:` 22 | `:O` or `:o` | :open_mouth: | `:open_mouth:` 23 | `xO` | :dizzy_face: | `:dizzy_face:` 24 | :| | :neutral_face: | `:neutral_face:` 25 | `B)` | :sunglasses: | `:sunglasses:` 26 | `:*` | :kissing: | `:kissing:` 27 | `<3` | :heart: | `:heart:` 28 | 29 | ## From Discord to Minecraft 30 | 31 | Discord | Minecraft | Discord Unicode 32 | --------|-----------|---------------- 33 | :smiley: | `:)` | `\ud83d\ude03` 34 | :smile: | `:D` | `\ud83d\ude04` 35 | :joy: | `;D` | `\ud83d\ude02` 36 | :laughing: | `xD` | `\ud83d\ude06` 37 | :frowning: | `:(` | `\ud83d\ude26` 38 | :sob: | `;(` | `\ud83d\ude2d` 39 | :tired_face: | `x(` | `\ud83d\ude2b` 40 | :wink: | `;)` | `\ud83d\ude09` 41 | :stuck_out_tongue: | `:P` | `\ud83d\ude1b` 42 | :stuck_out_tongue_winking_eye: | `;P` | `\ud83d\ude1c` 43 | :stuck_out_tongue_closed_eyes: | `xP` | `\ud83d\ude1d` 44 | :open_mouth: | `:O` | `\ud83d\ude2e` 45 | :dizzy_face: | `xO` | `\ud83d\ude35` 46 | :neutral_face: | :| | `\ud83d\ude10` 47 | :sunglasses: | `B)` | `\ud83d\ude0e` 48 | :kissing: | `:*` | `\ud83d\ude17` 49 | :heart: | `<3` | `\u2764` -------------------------------------------------------------------------------- /GETTING STARTED.md: -------------------------------------------------------------------------------- 1 | # GETTING STARTED 2 | 3 | ## How to setup for servers 4 | 1. Setup a Discord Application and a App Bot user (http://discordapp.com/developers/applications/me) 5 | 1. Allow the bot to access the channels you will use (https://discordapp.com/developers/docs/topics/oauth2#adding-bots-to-guilds) 6 | 1. Setup a Minecraft server with compatible [SpongeVanilla](https://docs.spongepowered.org/en/server/getting-started/implementations/spongevanilla.html) or [SpongeForge](https://docs.spongepowered.org/en/server/getting-started/implementations/spongeforge.html) 7 | 1. Download Discord Bridge [latest release](https://github.com/nguyenquyhy/DiscordBridge/releases) and put it in your server's mod folder 8 | 1. Start the server to create a default config file at `configs/discordbridge/config.json` 9 | 1. Set compulsory values in the newly created config file 10 | - `botToken`: token of your _App Bot User_ of your Bot in http://discordapp.com/developers/applications/me 11 | - `discordId` of each channel: the ID of your Discord channel. Check our 12 | [README.md](README.md) if you don't know how to obtain it. 13 | 1. Restart the server or run `/discord reload` 14 | 15 | ## How to use for players 16 | - You can chat in Discord in the any channel that has `minecraft` section set up. Your messages will be broadcast to all players if the server owners has set up a default/bot account. 17 | - You can chat in Minecraft: 18 | - Your messages will show up in the Discord channel under your Discord name if you have __authenticated__. 19 | - Otherwise, your messages will show up in the Discord channel under the bot name if you have not __authenticated__. 20 | - To __authenticate__, run this command in Minecraft `/discord login ` 21 | - **WARNING: the server may log all commands, so be careful not to leak your Discord credentials on untrusted servers.** 22 | - NOTE: when authenticated for the first time on a server, Discord will block the login attempt and send a notification email. You have to check your email for a link to unblock server's IP address. -------------------------------------------------------------------------------- /examples/config-multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokenStore": "JSON", 3 | "botToken": "APP_BOT_USER_TOKEN", 4 | "botDiscordGame": "Minecraft", 5 | "prefixBlacklist": ["!"], 6 | "ignoreBots": false, 7 | "minecraftBroadcastTemplate": "&2 %s", 8 | "channels": [ 9 | { 10 | "discordId": "DISCORD_PUBLIC_CHANNEL_ID", 11 | "discordInviteCode": "DISCORD_INVITATION_CODE", 12 | "discord": { 13 | "publicChat": { 14 | "authenticatedChatTemplate": "%s", 15 | "anonymousChatTemplate": "`%a:` %s" 16 | }, 17 | "broadcastTemplate": "_ %s_" 18 | }, 19 | "minecraft": { 20 | "chatTemplate": "&7<%a> &f%s", 21 | "attachment": { 22 | "template": "&3[Attachment]&r", 23 | "hoverTemplate": "Please check Discord.", 24 | "allowLink": false 25 | }, 26 | "emoji": { 27 | "template": "&b:%n:&r", 28 | "hoverTemplate": "Click to view emjoi.", 29 | "allowLink": true 30 | }, 31 | "mention": { 32 | "userTemplate": "@%s", 33 | "roleTemplate": "@%s", 34 | "everyoneTemplate": "&6@%s", 35 | "channelTemplate": "&9#%s" 36 | }, 37 | "roles": { 38 | "Admin": { 39 | "chatTemplate": "[&4ADMIN&r] &7<%a> &f%s", 40 | "attachment": { 41 | "hoverTemplate": "Click to open attachment.", 42 | "allowLink": true 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | { 49 | "discordId": "DISCORD_MONITOR_CHANNEL_ID", 50 | "discord": { 51 | "joinedTemplate": "_%s just joined the server_", 52 | "leftTemplate": "_%s just left the server_", 53 | "serverUpMessage": "Server has started.", 54 | "serverDownMessage": "Server has stopped.", 55 | "deathTemplate": "**%s**" 56 | } 57 | }, 58 | { 59 | "discordId": "DISCORD_STAFF_BROADCAST_CHANNEL_ID", 60 | "minecraft": { 61 | "chatTemplate": "&l&2 %s" 62 | } 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/database/JsonFileStorage.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.database; 2 | 3 | import ninja.leaping.configurate.ConfigurationNode; 4 | import ninja.leaping.configurate.gson.GsonConfigurationLoader; 5 | import ninja.leaping.configurate.loader.ConfigurationLoader; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.HashMap; 11 | import java.util.UUID; 12 | 13 | /** 14 | * Created by Hy on 1/6/2016. 15 | */ 16 | public class JsonFileStorage implements IStorage { 17 | private final ConfigurationLoader configLoader; 18 | private ConfigurationNode configNode; 19 | 20 | public JsonFileStorage(Path configDir) throws IOException { 21 | Path tokensFile = configDir.resolve("tokens.json"); 22 | configLoader = GsonConfigurationLoader.builder() 23 | .setPath(tokensFile) 24 | .setIndent(4) 25 | .setLenient(true) 26 | .build(); 27 | 28 | if (!Files.exists(tokensFile)) { 29 | Files.createFile(tokensFile); 30 | configNode = configLoader.load(); 31 | getCachedTokens().setValue(new HashMap()); 32 | configLoader.save(configNode); 33 | } else { 34 | configNode = configLoader.load(); 35 | } 36 | } 37 | 38 | @Override 39 | public void putToken(UUID player, String token) throws IOException { 40 | getCachedTokens().getNode(player.toString()).setValue(token); 41 | configLoader.save(configNode); 42 | } 43 | 44 | @Override 45 | public String getToken(UUID player) { 46 | return getCachedTokens().getNode(player.toString()).getString(); 47 | } 48 | 49 | @Override 50 | public void removeToken(UUID player) throws IOException { 51 | getCachedTokens().removeChild(player.toString()); 52 | configLoader.save(configNode); 53 | } 54 | 55 | private ConfigurationNode getCachedTokens() { 56 | return configNode.getNode("tokens"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/models/ChannelDiscordConfig.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.models; 2 | 3 | import ninja.leaping.configurate.objectmapping.Setting; 4 | import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | /** 8 | * Created by Hy on 10/13/2016. 9 | */ 10 | @ConfigSerializable 11 | public class ChannelDiscordConfig { 12 | /** 13 | * This is called only when the config file is first created. 14 | */ 15 | void initializeDefault() { 16 | joinedTemplate = "_%s just joined the server_"; 17 | leftTemplate = "_%s just left the server_"; 18 | publicChat = new SpongeChannelConfig(); 19 | publicChat.authenticatedChatTemplate = "%s"; 20 | publicChat.anonymousChatTemplate = "`%a:` %s"; 21 | serverUpMessage = "Server has started."; 22 | serverDownMessage = "Server has stopped."; 23 | broadcastTemplate = "_ %s_"; 24 | deathTemplate = "**%s**"; 25 | } 26 | 27 | @Setting 28 | public String joinedTemplate; 29 | @Setting 30 | public String leftTemplate; 31 | @Setting 32 | public SpongeChannelConfig publicChat; 33 | @Setting 34 | public SpongeChannelConfig staffChat; 35 | @Setting 36 | public String serverUpMessage; 37 | @Setting 38 | public String serverDownMessage; 39 | @Setting 40 | public String broadcastTemplate; 41 | @Setting 42 | public String deathTemplate; 43 | 44 | 45 | @Deprecated 46 | @Setting 47 | public String anonymousChatTemplate; 48 | @Deprecated 49 | @Setting 50 | public String authenticatedChatTemplate; 51 | 52 | void migrate() { 53 | if (StringUtils.isNotBlank(anonymousChatTemplate)) { 54 | if (publicChat == null) publicChat = new SpongeChannelConfig(); 55 | publicChat.anonymousChatTemplate = anonymousChatTemplate; 56 | anonymousChatTemplate = null; 57 | } 58 | if (StringUtils.isNotBlank(authenticatedChatTemplate)) { 59 | if (publicChat == null) publicChat = new SpongeChannelConfig(); 60 | publicChat.authenticatedChatTemplate = authenticatedChatTemplate; 61 | authenticatedChatTemplate = null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGE LOG 2 | 3 | ## 3.0.0 4 | - Change JavaCord to JDA for Discord API support 5 | - Upgrade to Minecraft 1.12 6 | 7 | ## 2.4.0 8 | - Support sending message to Discord when a player die (check `deathTemplate`). 9 | - Discord Bridge now ignores only messages from Discord Bridge and from the same server. Previously, the plugin ignores all messages from Discord Bridge regardless different servers. 10 | - Fix reloading does not reload templates. 11 | - Fix mentioning nickname. 12 | 13 | ## 2.3.0 14 | - Mentions in Discord show properly in Minecraft with configurable templates. 15 | - Mentions from Minecraft are supported with permission control. 16 | - Attachments in Discord show proper links in Minecraft. 17 | - Support Minecraft templates based on Discord roles. 18 | - Split `/discord reload` into `/discord reload` (reload config file only) and `/discord reconnect` (reconnect Discord connections). 19 | - Update Javacord to fix console spamming issue. 20 | 21 | ## 2.2.0 22 | - Set game activity of the bot. 23 | - Ignore Discord messages from all bots with `ignoreBots` and/or blacklist certain prefixes with `prefixBlacklist`. 24 | - Support One-Time Password. 25 | 26 | ## 2.1.0 27 | - Rename plugin ID from `com.nguyenquyhy.spongediscord` into `discordbridge`. 28 | - Add support for setting different templates for public chat and staff chat. Currently only Nucleus's staff chat is detected. Setting up `staffChat` section will route message from Minecraft to Discord. Check out example configurations to see the `publicChat` and `staffChat` section. 29 | - All Minecraft messages from other Minecraft channels (e.g. Clan chat) will not be routed to Discord. 30 | - Improve error message when the channel ID is incorrect. 31 | - Fix `/discord broadcast` 32 | 33 | ## 2.0.0 34 | - Configuration is now stored in `config.json`. Old `config.conf` will be migrated automatically. 35 | - Support for multiple channels. 36 | - Remove support for default account. Bot is compulsory now. 37 | - Remove support for Invite token. You have to add permission for the Bot to your channels before using the plugin. 38 | - `/discord login` command now accepts no parameters and will give out warning and instructions to proceed. 39 | - `/discord broadcast` command now uses templates in configuration. 40 | - Replace the underlying library for Discord API to reduce incompatibility with Sponge and Forge. 41 | 42 | ## 1.4.0 43 | - URL from Discord is clickable in Minecraft. 44 | - Emoji is translated properly between Discord and Minecraft. 45 | - Bot no longer tries to use invitation link. 46 | 47 | ## 1.3.1 48 | - Auto re-login for expired sessions on receiving new messages 49 | - Clean up error log 50 | 51 | ## 1.3.0 52 | - Rename to Discord Bridge 53 | - Update Discord4J 54 | - Escape player with underscore in their name 55 | 56 | ## 1.1.1 57 | - Update due to changes in Discord API. 58 | 59 | ## 1.1.0 60 | 61 | - Emojis are converted between Minecraft (`:)`, `:P`) and Discord format (`:smiley:`, `:smile:`). 62 | - Added permissions for `broadcast` and `reload` commands. 63 | 64 | ## 1.0.0 65 | 66 | - Player can now send/receive messages between Minecraft and a specific Discord channel. 67 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/commands/BroadcastCommand.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.commands; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 5 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 6 | import com.nguyenquyhy.discordbridge.utils.ErrorMessages; 7 | import com.nguyenquyhy.discordbridge.utils.TextUtil; 8 | import net.dv8tion.jda.core.JDA; 9 | import net.dv8tion.jda.core.entities.TextChannel; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.spongepowered.api.Sponge; 13 | import org.spongepowered.api.command.CommandException; 14 | import org.spongepowered.api.command.CommandResult; 15 | import org.spongepowered.api.command.CommandSource; 16 | import org.spongepowered.api.command.args.CommandContext; 17 | import org.spongepowered.api.command.spec.CommandExecutor; 18 | import org.spongepowered.api.entity.living.player.Player; 19 | import org.spongepowered.api.text.Text; 20 | import org.spongepowered.api.text.format.TextColors; 21 | 22 | /** 23 | * Created by Hy on 1/11/2016. 24 | */ 25 | public class BroadcastCommand implements CommandExecutor { 26 | @Override 27 | public CommandResult execute(CommandSource commandSource, CommandContext commandContext) throws CommandException { 28 | String message = commandContext.getOne("message").get(); 29 | boolean sent = broadcast(commandSource, message); 30 | return sent ? CommandResult.success() : CommandResult.empty(); 31 | } 32 | 33 | private boolean broadcast(CommandSource commandSource, String message) { 34 | DiscordBridge mod = DiscordBridge.getInstance(); 35 | GlobalConfig config = mod.getConfig(); 36 | Logger logger = mod.getLogger(); 37 | 38 | JDA defaultClient = mod.getBotClient(); 39 | if (defaultClient == null) { 40 | commandSource.sendMessage(Text.of(TextColors.RED, "You have to set up a Bot token first!")); 41 | return false; 42 | } 43 | 44 | // Send to Discord 45 | config.channels.stream().filter(channelConfig -> StringUtils.isNotBlank(channelConfig.discordId) 46 | && channelConfig.discord != null 47 | && StringUtils.isNotBlank(channelConfig.discord.broadcastTemplate)).forEach(channelConfig -> { 48 | TextChannel channel = defaultClient.getTextChannelById(channelConfig.discordId); 49 | if (channel != null) { 50 | String content = String.format(channelConfig.discord.broadcastTemplate, 51 | TextUtil.escapeForDiscord(message, channelConfig.discord.broadcastTemplate, "%s")); 52 | ChannelUtil.sendMessage(channel, content); 53 | logger.info("[BROADCAST DISCORD] " + message); 54 | } else { 55 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 56 | } 57 | }); 58 | 59 | // Send to Minecraft 60 | if (StringUtils.isNotBlank(config.minecraftBroadcastTemplate)) { 61 | for (Player player : Sponge.getServer().getOnlinePlayers()) { 62 | player.sendMessage(Text.join(TextUtil.formatUrl(String.format(config.minecraftBroadcastTemplate, message)))); 63 | } 64 | logger.info("[BROADCAST MINECRAFT] " + message); 65 | } 66 | return true; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/ColorUtil.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import org.spongepowered.api.text.format.TextColor; 4 | import org.spongepowered.api.text.format.TextColors; 5 | 6 | import java.awt.*; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class ColorUtil { 11 | private static Map minecraftColors = new HashMap<>(); 12 | private static Map textColors = new HashMap<>(); 13 | 14 | static { 15 | minecraftColors.put(new Color(0, 0, 0), "&0"); 16 | minecraftColors.put(new Color(0, 0, 170), "&1"); 17 | minecraftColors.put(new Color(0, 170, 0), "&2"); 18 | minecraftColors.put(new Color(0, 170, 170), "&3"); 19 | minecraftColors.put(new Color(170, 0, 0), "&4"); 20 | minecraftColors.put(new Color(170, 0, 170), "&5"); 21 | minecraftColors.put(new Color(255, 170, 0), "&6"); 22 | minecraftColors.put(new Color(170, 170, 170), "&7"); 23 | minecraftColors.put(new Color(85, 85, 85), "&8"); 24 | minecraftColors.put(new Color(85, 85, 255), "&9"); 25 | minecraftColors.put(new Color(85, 255, 85), "&a"); 26 | minecraftColors.put(new Color(85, 255, 255), "&b"); 27 | minecraftColors.put(new Color(255, 85, 85), "&c"); 28 | minecraftColors.put(new Color(255, 85, 255), "&d"); 29 | minecraftColors.put(new Color(255, 255, 85), "&e"); 30 | minecraftColors.put(new Color(255, 255, 255), "&f"); 31 | 32 | textColors.put(new Color(0, 0, 0), TextColors.BLACK); 33 | textColors.put(new Color(0, 0, 170), TextColors.DARK_BLUE); 34 | textColors.put(new Color(0, 170, 0), TextColors.DARK_GREEN); 35 | textColors.put(new Color(0, 170, 170), TextColors.DARK_AQUA); 36 | textColors.put(new Color(170, 0, 0), TextColors.DARK_RED); 37 | textColors.put(new Color(170, 0, 170), TextColors.DARK_PURPLE); 38 | textColors.put(new Color(255, 170, 0), TextColors.GOLD); 39 | textColors.put(new Color(170, 170, 170), TextColors.GRAY); 40 | textColors.put(new Color(85, 85, 85), TextColors.DARK_GRAY); 41 | textColors.put(new Color(85, 85, 255), TextColors.BLUE); 42 | textColors.put(new Color(85, 255, 85), TextColors.GREEN); 43 | textColors.put(new Color(85, 255, 255), TextColors.AQUA); 44 | textColors.put(new Color(255, 85, 85), TextColors.RED); 45 | textColors.put(new Color(255, 85, 255), TextColors.LIGHT_PURPLE); 46 | textColors.put(new Color(255, 255, 85), TextColors.YELLOW); 47 | textColors.put(new Color(255, 255, 255), TextColors.WHITE); 48 | } 49 | 50 | public static String getColorCode(Color color) { 51 | return minecraftColors.containsKey(color) ? minecraftColors.get(color) : ""; 52 | } 53 | 54 | public static TextColor getColor(Color color) { 55 | TextColor result = null; 56 | double minDistance = 0; 57 | for (Color mcColor : textColors.keySet()) { 58 | double distance = (mcColor.getRed() - color.getRed()) * (mcColor.getRed() - color.getRed()) 59 | + (mcColor.getGreen() - color.getGreen()) * (mcColor.getGreen() - color.getGreen()) 60 | + (mcColor.getBlue() - color.getBlue()) * (mcColor.getBlue() - color.getBlue()); 61 | if (result == null || minDistance > distance) { 62 | result = textColors.get(mcColor); 63 | minDistance = distance; 64 | } 65 | } 66 | return result; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/CommandRegistry.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge; 2 | 3 | import com.nguyenquyhy.discordbridge.commands.*; 4 | import org.spongepowered.api.command.args.GenericArguments; 5 | import org.spongepowered.api.command.spec.CommandSpec; 6 | import org.spongepowered.api.text.Text; 7 | 8 | /** 9 | * Created by Hy on 8/5/2016. 10 | */ 11 | public class CommandRegistry { 12 | /** 13 | * Register all commands 14 | */ 15 | public static void register() { 16 | CommandSpec loginCmd = CommandSpec.builder() 17 | .permission("discordbridge.login") 18 | .description(Text.of("(ADMIN) Login to your Discord account and bind to current Minecraft account")) 19 | .executor(new LoginCommand()) 20 | .build(); 21 | 22 | CommandSpec loginConfirmCmd = CommandSpec.builder() 23 | .permission("discordbridge.login") 24 | .arguments( 25 | GenericArguments.onlyOne(GenericArguments.string(Text.of("email"))), 26 | GenericArguments.onlyOne(GenericArguments.string(Text.of("password")))) 27 | .executor(new LoginConfirmCommand()) 28 | .build(); 29 | 30 | CommandSpec logoutCmd = CommandSpec.builder() 31 | .permission("discordbridge.login") 32 | .description(Text.of("Logout of your Discord account and unbind from current Minecraft account")) 33 | .executor(new LogoutCommand()) 34 | .build(); 35 | 36 | CommandSpec reloadCmd = CommandSpec.builder() 37 | .permission("discordbridge.reload") 38 | .description(Text.of("Reload Discord Bridge configuration")) 39 | .executor(new ReloadCommand()) 40 | .build(); 41 | 42 | CommandSpec reconnectCmd = CommandSpec.builder() 43 | .permission("discordbridge.reconnect") 44 | .description(Text.of("Reconnect Discord Bridge connection")) 45 | .executor(new ReconnectCommand()) 46 | .build(); 47 | 48 | CommandSpec broadcastCmd = CommandSpec.builder() 49 | .permission("discordbridge.broadcast") 50 | .description(Text.of("Broadcast message to Discord and online Minecraft accounts")) 51 | .arguments(GenericArguments.onlyOne(GenericArguments.string(Text.of("message")))) 52 | .executor(new BroadcastCommand()) 53 | .build(); 54 | 55 | CommandSpec statusCmd = CommandSpec.builder() 56 | .permission("discordbridge.status") 57 | .description(Text.of("Get status of current connections to Discord")) 58 | .executor(new StatusCommand()) 59 | .build(); 60 | 61 | CommandSpec otpCmd = CommandSpec.builder() 62 | .permission("discordbridge.login") 63 | .description(Text.of("Additional authorization for 2FA-enabled user accounts")) 64 | .arguments(GenericArguments.onlyOne(GenericArguments.integer(Text.of("code")))) 65 | .executor(new OtpCommand()) 66 | .build(); 67 | 68 | CommandSpec mainCommandSpec = CommandSpec.builder() 69 | //.permission("discordbridge") 70 | .description(Text.of("Discord in Minecraft")) 71 | .child(loginCmd, "login", "l") 72 | .child(loginConfirmCmd, "loginconfirm", "lc") 73 | .child(logoutCmd, "logout", "lo") 74 | .child(reloadCmd, "reload") 75 | .child(reconnectCmd, "reconnect") 76 | .child(broadcastCmd, "broadcast", "b", "bc") 77 | .child(statusCmd, "status", "s") 78 | .child(otpCmd, "otp", "o") 79 | .build(); 80 | 81 | DiscordBridge mod = DiscordBridge.getInstance(); 82 | mod.getGame().getCommandManager().register(mod, mainCommandSpec, "discord", "d"); 83 | 84 | mod.getLogger().info("/discord command registered."); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/listeners/ClientConnectionListener.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.listeners; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 5 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 6 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 7 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 8 | import com.nguyenquyhy.discordbridge.utils.ErrorMessages; 9 | import com.nguyenquyhy.discordbridge.utils.TextUtil; 10 | import net.dv8tion.jda.core.JDA; 11 | import net.dv8tion.jda.core.entities.TextChannel; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.spongepowered.api.entity.living.player.Player; 14 | import org.spongepowered.api.event.Listener; 15 | import org.spongepowered.api.event.network.ClientConnectionEvent; 16 | 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | 20 | /** 21 | * Created by Hy on 10/13/2016. 22 | */ 23 | public class ClientConnectionListener { 24 | @Listener 25 | public void onJoin(ClientConnectionEvent.Join event) { 26 | DiscordBridge mod = DiscordBridge.getInstance(); 27 | GlobalConfig config = mod.getConfig(); 28 | 29 | Optional player = event.getCause().first(Player.class); 30 | if (player.isPresent()) { 31 | UUID playerId = player.get().getUniqueId(); 32 | boolean loggingIn = false; 33 | if (!mod.getHumanClients().containsKey(playerId)) { 34 | loggingIn = LoginHandler.loginHumanAccount(player.get()); 35 | } 36 | 37 | if (!loggingIn && mod.getBotClient() != null) { 38 | // Use Bot client to send joined message 39 | for (ChannelConfig channelConfig : config.channels) { 40 | if (StringUtils.isNotBlank(channelConfig.discordId) 41 | && channelConfig.discord != null 42 | && StringUtils.isNotBlank(channelConfig.discord.joinedTemplate)) { 43 | TextChannel channel = mod.getBotClient().getTextChannelById(channelConfig.discordId); 44 | if (channel != null) { 45 | String content = String.format(channelConfig.discord.joinedTemplate, 46 | TextUtil.escapeForDiscord(player.get().getName(), channelConfig.discord.joinedTemplate, "%s")); 47 | ChannelUtil.sendMessage(channel, content); 48 | } else { 49 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | @Listener 58 | public void onDisconnect(ClientConnectionEvent.Disconnect event) { 59 | DiscordBridge mod = DiscordBridge.getInstance(); 60 | GlobalConfig config = mod.getConfig(); 61 | 62 | Optional player = event.getCause().first(Player.class); 63 | if (player.isPresent()) { 64 | UUID playerId = player.get().getUniqueId(); 65 | 66 | JDA client = mod.getHumanClients().get(playerId); 67 | if (client == null) client = mod.getBotClient(); 68 | if (client != null) { 69 | for (ChannelConfig channelConfig : config.channels) { 70 | if (StringUtils.isNotBlank(channelConfig.discordId) 71 | && channelConfig.discord != null 72 | && StringUtils.isNotBlank(channelConfig.discord.leftTemplate)) { 73 | TextChannel channel = client.getTextChannelById(channelConfig.discordId); 74 | if (channel != null) { 75 | String content = String.format(channelConfig.discord.leftTemplate, 76 | TextUtil.escapeForDiscord(player.get().getName(), channelConfig.discord.leftTemplate, "%s")); 77 | ChannelUtil.sendMessage(channel, content); 78 | } else { 79 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 80 | } 81 | } 82 | mod.removeAndLogoutClient(playerId); 83 | //unauthenticatedPlayers.remove(playerId); 84 | mod.getLogger().info(player.get().getName() + " has disconnected!"); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/logics/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.logics; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 5 | import com.nguyenquyhy.discordbridge.models.ChannelMinecraftConfigCore; 6 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 7 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 8 | import com.nguyenquyhy.discordbridge.utils.TextUtil; 9 | import net.dv8tion.jda.core.entities.*; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.spongepowered.api.Sponge; 13 | import org.spongepowered.api.text.Text; 14 | import org.spongepowered.api.text.action.TextActions; 15 | import org.spongepowered.api.text.serializer.TextSerializers; 16 | 17 | import java.net.MalformedURLException; 18 | import java.net.URL; 19 | import java.util.Collection; 20 | 21 | /** 22 | * Created by Hy on 8/6/2016. 23 | */ 24 | public class MessageHandler { 25 | /** 26 | * Forward Discord messages to Minecraft 27 | * 28 | * @param message 29 | */ 30 | public static void discordMessageReceived(Message message) { 31 | DiscordBridge mod = DiscordBridge.getInstance(); 32 | Logger logger = mod.getLogger(); 33 | GlobalConfig config = mod.getConfig(); 34 | 35 | for (ChannelConfig channelConfig : config.channels) { 36 | if (config.prefixBlacklist != null) { 37 | for (String prefix : config.prefixBlacklist) { 38 | if (StringUtils.isNotBlank(prefix) && message.getContent().startsWith(prefix)) { 39 | return; 40 | } 41 | } 42 | } 43 | if (config.ignoreBots && message.getAuthor().isBot()) { 44 | return; 45 | } 46 | if (message.getNonce() != null && message.getNonce().equals(ChannelUtil.SPECIAL_CHAR + ChannelUtil.BOT_RANDOM)) { 47 | return; 48 | } 49 | if (message.isPinned()) { 50 | return; 51 | } 52 | if (StringUtils.isNotBlank(channelConfig.discordId) 53 | && channelConfig.minecraft != null 54 | && message.getChannel() != null 55 | && message.getChannel().getId().equals(channelConfig.discordId)) { 56 | 57 | // Role base configuration 58 | ChannelMinecraftConfigCore minecraftConfig = channelConfig.minecraft; 59 | if (channelConfig.minecraft.roles != null) { 60 | Collection roles = message.getMember().getRoles(); 61 | for (String roleName : channelConfig.minecraft.roles.keySet()) { 62 | if (roles.stream().anyMatch(r -> r.getName().equals(roleName))) { 63 | ChannelMinecraftConfigCore roleConfig = channelConfig.minecraft.roles.get(roleName); 64 | roleConfig.inherit(channelConfig.minecraft); 65 | minecraftConfig = roleConfig; 66 | break; 67 | } 68 | } 69 | } 70 | 71 | if (StringUtils.isNotBlank(minecraftConfig.chatTemplate)) { 72 | Text messageText = TextUtil.formatForMinecraft(minecraftConfig, message); 73 | 74 | // Format attachments 75 | if (minecraftConfig.attachment != null 76 | && StringUtils.isNotBlank(minecraftConfig.attachment.template) 77 | && message.getAttachments() != null) { 78 | for (Message.Attachment attachment : message.getAttachments()) { 79 | String spacing = StringUtils.isBlank(message.getContent()) ? "" : " "; 80 | Text.Builder builder = Text.builder() 81 | .append(TextSerializers.FORMATTING_CODE.deserialize(spacing + minecraftConfig.attachment.template)); 82 | if (minecraftConfig.attachment.allowLink) { 83 | try { 84 | builder = builder.onClick(TextActions.openUrl(new URL(attachment.getUrl()))); 85 | } catch (MalformedURLException ignored) { 86 | 87 | } 88 | } 89 | if (StringUtils.isNotBlank(minecraftConfig.attachment.hoverTemplate)) 90 | builder = builder.onHover(TextActions.showText(Text.of(minecraftConfig.attachment.hoverTemplate))); 91 | messageText = Text.join(messageText, builder.build()); 92 | } 93 | } 94 | 95 | Text formattedMessage = messageText; 96 | // This case is used for default account 97 | logger.info(formattedMessage.toPlain()); 98 | Sponge.getServer().getOnlinePlayers().forEach(p -> p.sendMessage(formattedMessage)); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/logics/ConfigHandler.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.logics; 2 | 3 | import com.google.common.reflect.TypeToken; 4 | import com.nguyenquyhy.discordbridge.DiscordBridge; 5 | import com.nguyenquyhy.discordbridge.database.InMemoryStorage; 6 | import com.nguyenquyhy.discordbridge.database.JsonFileStorage; 7 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 8 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 9 | import com.nguyenquyhy.discordbridge.models.TokenStore; 10 | import com.nguyenquyhy.discordbridge.utils.ConfigUtil; 11 | import ninja.leaping.configurate.ConfigurationNode; 12 | import ninja.leaping.configurate.commented.CommentedConfigurationNode; 13 | import ninja.leaping.configurate.gson.GsonConfigurationLoader; 14 | import ninja.leaping.configurate.hocon.HoconConfigurationLoader; 15 | import ninja.leaping.configurate.objectmapping.ObjectMappingException; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.slf4j.Logger; 18 | 19 | import java.io.IOException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | 24 | /** 25 | * Created by Hy on 8/6/2016. 26 | */ 27 | public class ConfigHandler { 28 | public static GlobalConfig loadConfiguration() throws ObjectMappingException, IOException { 29 | DiscordBridge mod = DiscordBridge.getInstance(); 30 | Logger logger = mod.getLogger(); 31 | Path configDir = mod.getConfigDir(); 32 | 33 | if (!Files.exists(configDir)) { 34 | Files.createDirectories(configDir); 35 | } 36 | 37 | Path configFile = Paths.get(configDir + "/config.json"); 38 | 39 | GsonConfigurationLoader configLoader = GsonConfigurationLoader.builder().setPath(configFile).build(); 40 | ConfigurationNode configNode = configLoader.load(); 41 | 42 | GlobalConfig config = configNode.getValue(TypeToken.of(GlobalConfig.class), new GlobalConfig()); 43 | 44 | if (!Files.exists(configFile)) { 45 | Files.createFile(configFile); 46 | logger.info("Created default configuration!"); 47 | 48 | ChannelConfig channel = new ChannelConfig(); 49 | channel.initializeDefault(); 50 | 51 | Path legacyConfigFile = Paths.get(configDir + "/config.conf"); 52 | if (Files.exists(legacyConfigFile)) { 53 | logger.info("Migrating legacy config!"); 54 | CommentedConfigurationNode legacyConfigNode = HoconConfigurationLoader.builder().setPath(legacyConfigFile).build().load(); 55 | 56 | config.botToken = ConfigUtil.readString(legacyConfigNode, "BotToken", ""); 57 | String token = ConfigUtil.readString(legacyConfigNode, "TokenStore", "JSON"); 58 | switch (token) { 59 | case "NONE": 60 | config.tokenStore = TokenStore.NONE; 61 | break; 62 | case "InMemory": 63 | case "MEMORY": 64 | config.tokenStore = TokenStore.MEMORY; 65 | break; 66 | default: 67 | config.tokenStore = TokenStore.JSON; 68 | break; 69 | } 70 | channel.discordId = ConfigUtil.readString(legacyConfigNode, "Channel", ""); 71 | channel.discord.joinedTemplate = ConfigUtil.readString(legacyConfigNode, "JoinedMessageTemplate", "_%s just joined the server_"); 72 | channel.discord.leftTemplate = ConfigUtil.readString(legacyConfigNode, "LeftMessageTemplate", "_%s just left the server_"); 73 | channel.discord.publicChat.authenticatedChatTemplate = ConfigUtil.readString(legacyConfigNode, "MessageInDiscordTemplate", "%s"); 74 | channel.discord.publicChat.anonymousChatTemplate = ConfigUtil.readString(legacyConfigNode, "MessageInDiscordAnonymousTemplate", "_<%a>_ %s"); 75 | channel.discord.serverUpMessage = ConfigUtil.readString(legacyConfigNode, "MessageInDiscordServerUp", "Server has started."); 76 | channel.discord.serverDownMessage = ConfigUtil.readString(legacyConfigNode, "MessageInDiscordServerDown", "Server has stopped."); 77 | channel.minecraft.chatTemplate = ConfigUtil.readString(legacyConfigNode, "MessageInMinecraftTemplate", "&7<%a> &f%s"); 78 | } else { 79 | logger.info("Discord Bridge will not run until you have edited this file!"); 80 | } 81 | config.channels.add(channel); 82 | } 83 | 84 | config.migrate(); 85 | 86 | configNode.setValue(TypeToken.of(GlobalConfig.class), config); 87 | configLoader.save(configNode); 88 | logger.info("Configuration loaded."); 89 | 90 | if (config.channels.isEmpty() 91 | || !config.channels.stream().anyMatch(c -> !StringUtils.isBlank(c.discordId))) { 92 | logger.error("Channel ID is not set!"); 93 | } 94 | 95 | switch (config.tokenStore) { 96 | case MEMORY: 97 | mod.setStorage(new InMemoryStorage()); 98 | logger.info("Use InMemory storage."); 99 | break; 100 | case JSON: 101 | mod.setStorage(new JsonFileStorage(configDir)); 102 | logger.info("Use JSON storage."); 103 | break; 104 | default: 105 | logger.warn("No Token Store! Logging in will be disabled."); 106 | break; 107 | } 108 | return config; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/DiscordBridge.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge; 2 | 3 | import com.google.inject.Inject; 4 | import com.nguyenquyhy.discordbridge.database.IStorage; 5 | import com.nguyenquyhy.discordbridge.listeners.ChatListener; 6 | import com.nguyenquyhy.discordbridge.listeners.ClientConnectionListener; 7 | import com.nguyenquyhy.discordbridge.listeners.DeathListener; 8 | import com.nguyenquyhy.discordbridge.logics.ConfigHandler; 9 | import com.nguyenquyhy.discordbridge.logics.LoginHandler; 10 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 11 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 12 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 13 | import com.nguyenquyhy.discordbridge.utils.ErrorMessages; 14 | import net.dv8tion.jda.core.JDA; 15 | import net.dv8tion.jda.core.entities.*; 16 | import ninja.leaping.configurate.objectmapping.ObjectMappingException; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.slf4j.Logger; 19 | import org.spongepowered.api.Game; 20 | import org.spongepowered.api.Sponge; 21 | import org.spongepowered.api.config.ConfigDir; 22 | import org.spongepowered.api.event.Listener; 23 | import org.spongepowered.api.event.game.state.*; 24 | import org.spongepowered.api.plugin.Plugin; 25 | 26 | import java.io.IOException; 27 | import java.nio.file.Path; 28 | import java.util.*; 29 | 30 | /** 31 | * Created by Hy on 1/4/2016. 32 | */ 33 | @Plugin(id = "discordbridge", name = "Discord Bridge", version = "3.0.0", 34 | description = "A Sponge plugin to connect your Minecraft server with Discord", authors = {"Hy", "Mohron"}) 35 | public class DiscordBridge { 36 | 37 | private JDA consoleClient = null; 38 | private final Map humanClients = new HashMap<>(); 39 | private JDA botClient = null; 40 | 41 | private final Set unauthenticatedPlayers = new HashSet<>(100); 42 | 43 | @Inject 44 | private Logger logger; 45 | 46 | @Inject 47 | @ConfigDir(sharedRoot = false) 48 | private Path configDir; 49 | 50 | private GlobalConfig config; 51 | 52 | @Inject 53 | private Game game; 54 | 55 | private IStorage storage; 56 | 57 | private static DiscordBridge instance; 58 | 59 | @Listener 60 | public void onInitialization(GameInitializationEvent event) throws IOException, ObjectMappingException { 61 | instance = this; 62 | config = ConfigHandler.loadConfiguration(); 63 | 64 | Sponge.getEventManager().registerListeners(this, new ChatListener()); 65 | Sponge.getEventManager().registerListeners(this, new ClientConnectionListener()); 66 | Sponge.getEventManager().registerListeners(this, new DeathListener()); 67 | } 68 | 69 | @Listener 70 | public void onServerStart(GameStartedServerEvent event) { 71 | CommandRegistry.register(); 72 | LoginHandler.loginBotAccount(); 73 | } 74 | 75 | @Listener 76 | public void onServerStop(GameStoppingServerEvent event) { 77 | if (botClient != null) { 78 | for (ChannelConfig channelConfig : config.channels) { 79 | if (StringUtils.isNotBlank(channelConfig.discordId) 80 | && channelConfig.discord != null 81 | && StringUtils.isNotBlank(channelConfig.discord.serverDownMessage)) { 82 | TextChannel channel = botClient.getTextChannelById(channelConfig.discordId); 83 | if (channel != null) { 84 | ChannelUtil.sendMessage(channel, channelConfig.discord.serverDownMessage); 85 | } else { 86 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | public static DiscordBridge getInstance() { 94 | return instance; 95 | } 96 | 97 | public Game getGame() { 98 | return game; 99 | } 100 | 101 | public Path getConfigDir() { 102 | return configDir; 103 | } 104 | 105 | public GlobalConfig getConfig() { 106 | return config; 107 | } 108 | 109 | public void setConfig(GlobalConfig config) { 110 | this.config = config; 111 | } 112 | 113 | public Logger getLogger() { 114 | return logger; 115 | } 116 | 117 | public IStorage getStorage() { 118 | return storage; 119 | } 120 | 121 | public void setStorage(IStorage storage) { 122 | this.storage = storage; 123 | } 124 | 125 | public JDA getBotClient() { 126 | return botClient; 127 | } 128 | 129 | public void setBotClient(JDA botClient) { 130 | this.botClient = botClient; 131 | } 132 | 133 | public Map getHumanClients() { 134 | return humanClients; 135 | } 136 | 137 | public Set getUnauthenticatedPlayers() { 138 | return unauthenticatedPlayers; 139 | } 140 | 141 | public void addClient(UUID player, JDA client) { 142 | if (player == null) { 143 | consoleClient = client; 144 | } else { 145 | humanClients.put(player, client); 146 | } 147 | } 148 | 149 | public void removeAndLogoutClient(UUID player) { 150 | if (player == null) { 151 | consoleClient.shutdown(); 152 | consoleClient = null; 153 | } else { 154 | if (humanClients.containsKey(player)) { 155 | JDA client = humanClients.get(player); 156 | client.shutdown(); 157 | humanClients.remove(player); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/listeners/ChatListener.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.listeners; 2 | 3 | import com.nguyenquyhy.discordbridge.DiscordBridge; 4 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 5 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 6 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 7 | import com.nguyenquyhy.discordbridge.utils.ErrorMessages; 8 | import com.nguyenquyhy.discordbridge.utils.TextUtil; 9 | import net.dv8tion.jda.core.JDA; 10 | import net.dv8tion.jda.core.entities.TextChannel; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.spongepowered.api.entity.living.player.Player; 13 | import org.spongepowered.api.event.Listener; 14 | import org.spongepowered.api.event.Order; 15 | import org.spongepowered.api.event.message.MessageChannelEvent; 16 | import org.spongepowered.api.text.Text; 17 | import org.spongepowered.api.text.channel.MessageChannel; 18 | 19 | import java.util.Optional; 20 | import java.util.UUID; 21 | 22 | /** 23 | * Created by Hy on 10/13/2016. 24 | */ 25 | public class ChatListener { 26 | DiscordBridge mod = DiscordBridge.getInstance(); 27 | 28 | /** 29 | * Send chat from Minecraft to Discord 30 | * 31 | * @param event 32 | */ 33 | @Listener(order = Order.LATE) 34 | public void onChat(MessageChannelEvent.Chat event) { 35 | 36 | if (event.isCancelled() || event.isMessageCancelled()) return; 37 | 38 | sendToDiscord(event); 39 | formatForMinecraft(event); 40 | } 41 | 42 | private void sendToDiscord(MessageChannelEvent.Chat event) { 43 | GlobalConfig config = mod.getConfig(); 44 | 45 | boolean isStaffChat = false; 46 | if (event.getChannel().isPresent()) { 47 | MessageChannel channel = event.getChannel().get(); 48 | if (channel.getClass().getName().equals("io.github.nucleuspowered.nucleus.modules.staffchat.StaffChatMessageChannel")) 49 | isStaffChat = true; 50 | else if (!channel.getClass().getName().startsWith("org.spongepowered.api.text.channel.MessageChannel")) 51 | return; // Ignore all other types 52 | } 53 | 54 | String plainString = event.getRawMessage().toPlain().trim(); 55 | if (StringUtils.isBlank(plainString) || plainString.startsWith("/")) return; 56 | 57 | plainString = TextUtil.formatMinecraftMessage(plainString); 58 | Optional player = event.getCause().first(Player.class); 59 | 60 | if (player.isPresent()) { 61 | UUID playerId = player.get().getUniqueId(); 62 | 63 | JDA client = mod.getBotClient(); 64 | boolean isBotAccount = true; 65 | if (mod.getHumanClients().containsKey(playerId)) { 66 | client = mod.getHumanClients().get(playerId); 67 | isBotAccount = false; 68 | } 69 | 70 | if (client != null) { 71 | for (ChannelConfig channelConfig : config.channels) { 72 | if (StringUtils.isNotBlank(channelConfig.discordId) && channelConfig.discord != null) { 73 | String template = null; 74 | if (!isStaffChat && channelConfig.discord.publicChat != null) { 75 | template = isBotAccount ? channelConfig.discord.publicChat.anonymousChatTemplate : channelConfig.discord.publicChat.authenticatedChatTemplate; 76 | } else if (isStaffChat && channelConfig.discord.staffChat != null) { 77 | template = isBotAccount ? channelConfig.discord.staffChat.anonymousChatTemplate : channelConfig.discord.staffChat.authenticatedChatTemplate; 78 | } 79 | 80 | if (StringUtils.isNotBlank(template)) { 81 | TextChannel channel = client.getTextChannelById(channelConfig.discordId); 82 | 83 | if (channel == null) { 84 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 85 | return; 86 | } 87 | 88 | // Format Mentions for Discord 89 | plainString = TextUtil.formatMinecraftMention(plainString, channel.getGuild(), player.get(), isBotAccount); 90 | 91 | if (isBotAccount) { 92 | // if (channel == null) { 93 | // LoginHandler.loginBotAccount(); 94 | // } 95 | String content = String.format( 96 | template.replace("%a", 97 | TextUtil.escapeForDiscord(player.get().getName(), template, "%a")), 98 | plainString); 99 | ChannelUtil.sendMessage(channel, content); 100 | } else { 101 | // if (channel == null) { 102 | // LoginHandler.loginHumanAccount(player.get()); 103 | // } 104 | ChannelUtil.sendMessage(channel, String.format(template, plainString)); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | private void formatForMinecraft(MessageChannelEvent.Chat event) { 114 | Text rawMessage = event.getRawMessage(); 115 | Optional player = event.getCause().first(Player.class); 116 | 117 | if (player.isPresent()) { 118 | /* UUID playerId = player.get().getUniqueId(); 119 | 120 | for (ChannelConfig channelConfig : config.channels) { 121 | String template = null; 122 | 123 | Channel channel = client.getChannelById(channelConfig.discordId); 124 | 125 | Optional userOptional = DiscordUtil.getUserByName(player.get().getName(), channel.getServer()); 126 | if (userOptional.isPresent()) { 127 | User user = userOptional.get(); 128 | } 129 | 130 | ChannelMinecraftConfigCore minecraftConfig = channelConfig.minecraft; 131 | if (channelConfig.minecraft.roles != null) { 132 | Collection roles = message.getAuthor().getRoles(message.getChannelReceiver().getServer()); 133 | for (String roleName : channelConfig.minecraft.roles.keySet()) { 134 | if (roles.stream().anyMatch(r -> r.getName().equals(roleName))) { 135 | ChannelMinecraftConfigCore roleConfig = channelConfig.minecraft.roles.get(roleName); 136 | roleConfig.inherit(channelConfig.minecraft); 137 | minecraftConfig = roleConfig; 138 | break; 139 | } 140 | } 141 | } 142 | } 143 | 144 | event.setMessage(rawMessage);*/ 145 | } 146 | 147 | } 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Bridge 2 | This is a [Sponge](http://spongepowered.com) plugin to integrate [Minecraft](https://minecraft.net) server with a [Discord](https://discordapp.com) channel. 3 | 4 | ## Features 5 | 6 | - Player's chat messages in Minecraft are sent to specified Discord channels, and chat messages in specific Discord channels are also sent to online players in Minecraft. 7 | - Admins and mods can log in to their own Discord account, so that chat messages show under their names in Discord. 8 | - Emoji is converted between Minecraft and Discord format. Details are showed in [EMOJI.md](EMOJI.md). 9 | - Clickable URL. 10 | - Multiple channels with custom configuration for each channel. E.g.: 11 | - 1 public channel to send & receive messages between Discord and Minecraft 12 | - 1 monitoring channel to record only server start/stop and player join/leave events 13 | - 1 staff-only channel that send message one-way from Discord to Minecraft with a special announcement template 14 | - Set game activity of the bot 15 | - Ignore Discord messages from all bots and/or blacklist certain prefixes 16 | - Support One-Time Password 17 | - **New in 2.3.0** 18 | - Mentions in Discord show properly in Minecraft with configurable templates 19 | - Mentions from Minecraft are supported with permission control (check **Additional Permissions**) 20 | - Attachments in Discord shows proper links in Minecraft 21 | - Support Minecraft templates based on Discord roles 22 | 23 | ## Getting Started for server owners and players 24 | 25 | [GETTING STARTED.md](GETTING STARTED.md) 26 | 27 | ## Migrating from 1.x.x or 2.0.0 28 | 29 | [MIGRATE.md](MIGRATE.md) 30 | 31 | ## Build your own .jar 32 | 33 | 1. Clone this repository 34 | 1. Run `gradlew` 35 | 1. The jar file will be in `build/libs/DiscordBridge-{version}-all.jar`. 36 | 37 | ## Commands 38 | 39 | - `/discord login`: login to Discord and bind the Discord account to the Minecraft account for automatic login in the future. The email and password will not be stored; instead, the access token of the user will be stored in the config folder on the server. 40 | - `/discord otp`: One-time password for Discord login _(thanks Prototik)_. 41 | - `/discord logout`: logout of Discord and unbind the Discord account from the Minecraft account. 42 | - `/discord broadcast`: as this plugin cannot capture server's `/say` at the moment, this command is to send a message to all online players and Discord. This command requires having the default account set up. 43 | - `/discord status`: show current connection status. 44 | - `/discord reload`: reload configurations. 45 | - `/discord reconnect`: reconnect Discord connection. 46 | 47 | A short summary is below: 48 | 49 | | Command | Shorthand | Permission | 50 | |---------|-----------|------------| 51 | | `/discord login` | `/d l` | `discordbridge.login` | 52 | | `/discord otp` | `/d otp` | `discordbridge.login` | 53 | | `/discord logout` | `/d lo` | `discordbridge.login` | 54 | | `/discord broadcast ` | `/d b ` | `discordbridge.broadcast` | 55 | | `/discord status` | `/d s` | `discordbridge.status` | 56 | | `/discord reload` | `/d reload` | `discordbridge.reload` | 57 | | `/discord reconnect` | `/d reconnect` | `discordbridge.reconnect` | 58 | 59 | Some ideas for future commands 60 | 61 | | Command | Note | 62 | |---------|------| 63 | | `/discord config` | Show current configuration | 64 | | `/discord status` | Show current Discord account | 65 | 66 | ## Configurations 67 | 68 | Configuration is stored in `config.json` file. 69 | 70 | - Global config 71 | - `botToken`: App Bot User's token 72 | - `botDiscordGame`: sets the current game activity of the bot in Discord _(thanks, Vankka)_ 73 | - `tokenStore`: `JSON` (default) or `NONE` (user authentication will be disabled) or `InMemory` (mainly for testing). This is used for player authentication. 74 | - `minecraftBroadcastTemplate`: template for messages in Minecraft from `/discord broadcast` command 75 | - `prefixBlacklist`: a list of prefix string (e.g. `["!"]`) that will be ignored by the plugin _(thanks, Vankka)_ 76 | - `ignoreBots`: ignore all messages from any Discord Bots _(thanks, Vankka)_ 77 | - `channels`: a list of channel configurations 78 | - Channel config 79 | - `discordId`: the ID of the Discord channel (usually a 18-digit number) 80 | - `discord`: templates in Discord 81 | - `joinedTemplate`: (optional) template for a message in Discord when a player joins the server 82 | - `leftTemplate`: (optional) template for a message in Discord when a player leaves the server 83 | - `anonymousChatTemplate`: (optional) template for messages from Minecraft to Discord for unauthenticated user 84 | - `authenticatedChatTemplate`: (optional) template for messages from Minecraft to Discord for authenticated user 85 | - `broadcastTemplate`: (optional) template for messages in Discord from `/discord broadcast` command 86 | - `deathTemplate`: (optional) template for a message in Discord when a player dies _(thanks, Mohron)_ 87 | - `minecraft`: templates in Minecraft 88 | - `chatTemplate`: (optional) template for messages from Discord to Minecraft. For supporting placeholders in the template, check the section **Chat placeholder** 89 | - `attachment`: _(thanks, Mohron)_ 90 | - `template`: template for Discord attachments linked in Minecraft 91 | - `hoverTemplate`: template for the message shown when you hover over an attachment link 92 | - `allowLink`: adds a clickable link in game for attachments sent via discord 93 | - `emoji`: _(thanks, Mohron)_ 94 | - `template`: template for custom emoji viewed in Minecraft - accepts `%n` 95 | - `hoverTemplate`: template for the message shown when you hover over an emoji 96 | - `allowLink`: adds a clickable link in game to view the emoji image 97 | - `mention`: _(thanks, Mohron)_ 98 | - `userTemplate`: template for @user mentions - accepts `%s`/`%u` 99 | - `roleTemplate`: template for @role mentions - accepts `%s` 100 | - `everyoneTemplate`: template for @here & @everyone mentions - accepts `%s` 101 | - `channelTemplate`: template for @here & @everyone mentions - accepts `%s` 102 | - `roles`: `minecraft` configurations that are for a specific Discord role 103 | 104 | You can find some example configurations in `examples` folders. 105 | 106 | ### Chat Placeholders 107 | - `%s` - the message sent via discord 108 | - `%a` - the nickname of the message author or username if nickname is unavailable 109 | - `%u` - the username of the author. This is used if you want to disallow Discord nickname. 110 | - `%r` - the name of the highest Discord role held by the message author. Color of the role will also be translated into Minecraft color automatically. 111 | - `%g` - the current game of the message author 112 | - `%n` - the name of of custom emoji 113 | 114 | ### Additional Permissions 115 | *NOTE: The below permissions are applicable only to unathenticated users. Authenticated users chat under their own Discord accounts, so you can restrict using Text permission of Discord roles.* 116 | 117 | | Permission | Use | 118 | |---------|-----------| 119 | | `discordbridge.mention.name`
`discordbridge.mention.name.` | Allows `@username`/`@nickname` mentions to be sent from Minecraft | 120 | | `discordbridge.mention.role`
`discordbridge.mention.role.` | Allows `@role` mentions - the role must have "Allow anyone to @mention" set | 121 | | `discordbridge.mention.channel`
`discordbridge.mention.channel.` | Allows `#channel` mention | 122 | | `discordbridge.mention.here` | Allows the `@here` mention1 | 123 | | `discordbridge.mention.everyone` | Allows the `@everyone` mention1 | 124 | > 1 The bot must have permission to "Mention Everyone" in order to use `@here` & `@everyone`. 125 | 126 | ## Frequently Asked Questions 127 | 128 | ### How to get channel ID 129 | 130 | 1. Open `User Settings` in Discord, then open `Appearance` section and tick `Developer Mode` 131 | 1. Right click any channel and click `Copy ID` 132 | 133 | ## CHANGELOG 134 | 135 | [CHANGELOG.md](CHANGELOG.md) 136 | 137 | ## TODO 138 | 139 | * 2.4.0 140 | - [ ] New config to allow executing Minecraft command from Discord 141 | 142 | * Future 143 | - [ ] MySQL token store 144 | - [ ] Group-based prefix 145 | - [ ] Handle custom Sponge channels (e.g. MCClan and staff chat of Nucleus) 146 | - [ ] A command to check Bot connection status 147 | - [ ] New config to route Minecraft server log to Discord 148 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/logics/LoginHandler.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.logics; 2 | 3 | import com.mashape.unirest.http.HttpResponse; 4 | import com.mashape.unirest.http.JsonNode; 5 | import com.mashape.unirest.http.Unirest; 6 | import com.mashape.unirest.http.exceptions.UnirestException; 7 | import com.nguyenquyhy.discordbridge.DiscordBridge; 8 | import com.nguyenquyhy.discordbridge.database.IStorage; 9 | import com.nguyenquyhy.discordbridge.models.ChannelConfig; 10 | import com.nguyenquyhy.discordbridge.models.GlobalConfig; 11 | import com.nguyenquyhy.discordbridge.utils.ChannelUtil; 12 | import com.nguyenquyhy.discordbridge.utils.ErrorMessages; 13 | import net.dv8tion.jda.core.AccountType; 14 | import net.dv8tion.jda.core.JDA; 15 | import net.dv8tion.jda.core.JDABuilder; 16 | import net.dv8tion.jda.core.entities.*; 17 | import net.dv8tion.jda.core.exceptions.RateLimitedException; 18 | import org.apache.commons.lang3.StringUtils; 19 | import org.json.JSONObject; 20 | import org.slf4j.Logger; 21 | import org.spongepowered.api.command.CommandResult; 22 | import org.spongepowered.api.command.CommandSource; 23 | import org.spongepowered.api.command.source.CommandBlockSource; 24 | import org.spongepowered.api.command.source.ConsoleSource; 25 | import org.spongepowered.api.entity.living.player.Player; 26 | import org.spongepowered.api.text.Text; 27 | import org.spongepowered.api.text.format.TextColors; 28 | import org.spongepowered.api.text.format.TextStyles; 29 | 30 | import javax.security.auth.login.LoginException; 31 | import java.io.IOException; 32 | import java.util.HashMap; 33 | import java.util.Map; 34 | import java.util.UUID; 35 | 36 | /** 37 | * Created by Hy on 8/6/2016. 38 | */ 39 | public class LoginHandler { 40 | private static DiscordBridge mod = DiscordBridge.getInstance(); 41 | private static Logger logger = mod.getLogger(); 42 | 43 | private static Map MFA_TICKETS = new HashMap<>(); 44 | 45 | public static boolean loginBotAccount() { 46 | GlobalConfig config = mod.getConfig(); 47 | 48 | if (StringUtils.isBlank(config.botToken)) { 49 | logger.warn("No Bot token is available! Messages can only get from and to authenticated players."); 50 | return false; 51 | } 52 | 53 | JDA defaultClient = mod.getBotClient(); 54 | if (defaultClient != null && defaultClient.getToken().equals(config.botToken)) { 55 | return true; 56 | } 57 | 58 | if (defaultClient != null) { 59 | defaultClient.shutdown(); 60 | } 61 | 62 | logger.info("Logging in to bot Discord account..."); 63 | 64 | try { 65 | prepareBotClient(config.botToken, null); 66 | return true; 67 | } catch (LoginException e) { 68 | e.printStackTrace(); 69 | } catch (RateLimitedException e) { 70 | e.printStackTrace(); 71 | } catch (InterruptedException e) { 72 | e.printStackTrace(); 73 | } 74 | return false; 75 | } 76 | 77 | /** 78 | * @param player 79 | * @return 80 | */ 81 | public static boolean loginHumanAccount(Player player) { 82 | IStorage storage = mod.getStorage(); 83 | 84 | if (storage != null) { 85 | String cachedToken = mod.getStorage().getToken(player.getUniqueId()); 86 | if (StringUtils.isNotBlank(cachedToken)) { 87 | player.sendMessage(Text.of(TextColors.GRAY, "Logging in to Discord...")); 88 | 89 | JDA client = mod.getHumanClients().get(player.getUniqueId()); 90 | if (client != null) { 91 | client.shutdown(); 92 | } 93 | return PrepareHumanClientForCommandSource(player, cachedToken); 94 | } 95 | } 96 | return false; 97 | } 98 | 99 | public static CommandResult login(CommandSource commandSource, String email, String password) { 100 | logout(commandSource, true); 101 | 102 | try { 103 | HttpResponse response = Unirest.post("https://discordapp.com/api/v6/auth/login") 104 | .header("content-type", "application/json") 105 | .body(new JSONObject().put("email", email).put("password", password)) 106 | .asJson(); 107 | if (response.getStatus() != 200) { 108 | DiscordBridge.getInstance().getLogger().info("Auth response {} code with: {}", response.getStatus(), response.getBody()); 109 | commandSource.sendMessage(Text.of(TextColors.RED, "Wrong email or password!")); 110 | return CommandResult.empty(); 111 | } 112 | JSONObject result = response.getBody().getObject(); 113 | if (result.has("mfa") && result.getBoolean("mfa")) { 114 | MFA_TICKETS.put(commandSource, result.getString("ticket")); 115 | commandSource.sendMessage(Text.of(TextColors.GREEN, "Additional authorization required! Please type '/discord otp ' within a code from your authorization app")); 116 | } else if (result.has("token")) { 117 | String token = result.getString("token"); 118 | 119 | if (PrepareHumanClientForCommandSource(commandSource, token)) return CommandResult.success(); 120 | } else { 121 | commandSource.sendMessage(Text.of(TextColors.RED, "Unexpected error!")); 122 | } 123 | } catch (UnirestException e) { 124 | e.printStackTrace(); 125 | commandSource.sendMessage(Text.of(TextColors.RED, "Unexpected error!")); 126 | } 127 | return CommandResult.empty(); 128 | } 129 | 130 | public static CommandResult otp(CommandSource commandSource, int code) { 131 | String ticket = MFA_TICKETS.remove(commandSource); 132 | if (ticket == null) { 133 | commandSource.sendMessage(Text.of(TextColors.RED, "No OTP auth queued!")); 134 | return CommandResult.empty(); 135 | } 136 | try { 137 | HttpResponse response = Unirest.post("https://discordapp.com/api/v6/auth/mfa/totp") 138 | .header("content-type", "application/json") 139 | .body(new JSONObject().put("code", String.format("%06d", code)).put("ticket", ticket)) 140 | .asJson(); 141 | if (response.getStatus() != 200) { 142 | commandSource.sendMessage(Text.of(TextColors.RED, "Wrong auth code! Retry with '/discord loginconfirm '")); 143 | return CommandResult.empty(); 144 | } 145 | String token = response.getBody().getObject().getString("token"); 146 | if (PrepareHumanClientForCommandSource(commandSource, token)) return CommandResult.success(); 147 | } catch (UnirestException e) { 148 | e.printStackTrace(); 149 | commandSource.sendMessage(Text.of(TextColors.RED, "Unexpected error!")); 150 | } 151 | return CommandResult.empty(); 152 | } 153 | 154 | public static boolean PrepareHumanClientForCommandSource(CommandSource commandSource, String token) { 155 | try { 156 | prepareHumanClient(token, commandSource); 157 | return true; 158 | } catch (LoginException e) { 159 | e.printStackTrace(); 160 | logger.error("Cannot connect to Discord!", e); 161 | if (commandSource != null) { 162 | commandSource.sendMessage(Text.of(TextColors.RED, "Unable to login! Please check your login details or your email for login verification.")); 163 | } 164 | } catch (InterruptedException | RateLimitedException e) { 165 | e.printStackTrace(); 166 | logger.error("Cannot connect to Discord!", e); 167 | if (commandSource != null) { 168 | commandSource.sendMessage(Text.of(TextColors.RED, "Unable to login! Please try again later.")); 169 | } 170 | } 171 | return false; 172 | } 173 | 174 | public static CommandResult logout(CommandSource commandSource, boolean isSilence) { 175 | if (commandSource instanceof Player) { 176 | Player player = (Player) commandSource; 177 | UUID playerId = player.getUniqueId(); 178 | try { 179 | DiscordBridge.getInstance().getStorage().removeToken(playerId); 180 | } catch (IOException e) { 181 | e.printStackTrace(); 182 | commandSource.sendMessage(Text.of(TextColors.RED, "Cannot remove cached token!")); 183 | } 184 | mod.removeAndLogoutClient(playerId); 185 | mod.getUnauthenticatedPlayers().add(player.getUniqueId()); 186 | 187 | if (!isSilence) 188 | commandSource.sendMessage(Text.of(TextColors.YELLOW, "Logged out of Discord!")); 189 | return CommandResult.success(); 190 | } else if (commandSource instanceof ConsoleSource) { 191 | mod.removeAndLogoutClient(null); 192 | commandSource.sendMessage(Text.of("Logged out of Discord!")); 193 | return CommandResult.success(); 194 | } else if (commandSource instanceof CommandBlockSource) { 195 | commandSource.sendMessage(Text.of(TextColors.YELLOW, "Cannot log out from command blocks!")); 196 | return CommandResult.empty(); 197 | } 198 | return CommandResult.empty(); 199 | } 200 | 201 | private static JDA prepareBotClient(String botToken, CommandSource commandSource) throws LoginException, RateLimitedException, InterruptedException { 202 | GlobalConfig config = mod.getConfig(); 203 | 204 | if (commandSource != null) 205 | commandSource.sendMessage(Text.of(TextColors.GOLD, TextStyles.BOLD, "Logging in...")); 206 | 207 | JDA client = new JDABuilder(AccountType.BOT) 208 | .setToken(botToken) 209 | .addEventListener(new MessageHandler()) 210 | .buildBlocking(); 211 | 212 | User user = client.getSelfUser(); 213 | String name = "unknown"; 214 | if (user != null) 215 | name = user.getName(); 216 | String text = "Bot account " + name + " will be used for all unauthenticated users!"; 217 | if (StringUtils.isNotBlank(config.botDiscordGame)) { 218 | client.getPresence().setGame(Game.playing(config.botDiscordGame)); 219 | } 220 | if (commandSource != null) 221 | commandSource.sendMessage(Text.of(TextColors.GOLD, TextStyles.BOLD, text)); 222 | else 223 | logger.info(text); 224 | 225 | mod.setBotClient(client); 226 | 227 | for (ChannelConfig channelConfig : config.channels) { 228 | if (StringUtils.isNotBlank(channelConfig.discordId)) { 229 | TextChannel channel = client.getTextChannelById(channelConfig.discordId); 230 | if (channel != null) { 231 | channelJoined(client, config, channelConfig, channel, commandSource); 232 | } else { 233 | ErrorMessages.CHANNEL_NOT_FOUND.log(channelConfig.discordId); 234 | } 235 | } else { 236 | logger.warn("Channel with empty ID!"); 237 | } 238 | } 239 | 240 | return client; 241 | } 242 | 243 | private static JDA prepareHumanClient(String cachedToken, CommandSource commandSource) throws LoginException, InterruptedException, RateLimitedException { 244 | if (commandSource instanceof CommandBlockSource) { 245 | commandSource.sendMessage(Text.of(TextColors.GREEN, "Account is valid!")); 246 | return null; 247 | } 248 | 249 | GlobalConfig config = mod.getConfig(); 250 | 251 | JDA client = new JDABuilder(AccountType.CLIENT) 252 | .setToken(cachedToken) 253 | .buildBlocking(); 254 | 255 | try { 256 | String name = client.getSelfUser().getName(); 257 | commandSource.sendMessage(Text.of(TextColors.GOLD, TextStyles.BOLD, "You have logged in to Discord account " + name + "!")); 258 | 259 | if (commandSource instanceof Player) { 260 | Player player = (Player) commandSource; 261 | UUID playerId = player.getUniqueId(); 262 | mod.getUnauthenticatedPlayers().remove(playerId); 263 | mod.addClient(playerId, client); 264 | mod.getStorage().putToken(playerId, client.getToken()); 265 | } else if (commandSource instanceof ConsoleSource) { 266 | commandSource.sendMessage(Text.of("WARNING: This Discord account will be used only for this console session!")); 267 | mod.addClient(null, client); 268 | } 269 | 270 | for (ChannelConfig channelConfig : config.channels) { 271 | if (StringUtils.isNotBlank(channelConfig.discordId)) { 272 | TextChannel channel = client.getTextChannelById(channelConfig.discordId); 273 | if (channel != null) { 274 | channelJoined(client, config, channelConfig, channel, commandSource); 275 | } else { 276 | ErrorMessages.CHANNEL_NOT_FOUND_HUMAN.log(channelConfig.discordId); 277 | } 278 | } else { 279 | logger.warn("Channel with empty ID!"); 280 | } 281 | } 282 | } catch (IOException e) { 283 | logger.error("Cannot connect to Discord!", e); 284 | } 285 | 286 | return client; 287 | } 288 | 289 | private static void channelJoined(JDA client, GlobalConfig config, ChannelConfig channelConfig, TextChannel channel, CommandSource src) { 290 | 291 | if (channel != null && StringUtils.isNotBlank(channelConfig.discordId) && channelConfig.discord != null) { 292 | if (client != mod.getBotClient()) { 293 | String playerName = "console"; 294 | if (src instanceof Player) { 295 | Player player = (Player) src; 296 | playerName = player.getName(); 297 | } 298 | if (StringUtils.isNotBlank(channelConfig.discord.joinedTemplate)) { 299 | String content = String.format(channelConfig.discord.joinedTemplate, playerName); 300 | ChannelUtil.sendMessage(channel, content); 301 | } 302 | logger.info(playerName + " connected to Discord channel " + channelConfig.discordId + "."); 303 | } else { 304 | logger.info("Bot account has connected to Discord channel " + channelConfig.discordId + "."); 305 | if (StringUtils.isNotBlank(channelConfig.discord.serverUpMessage)) { 306 | ChannelUtil.sendMessage(channel, channelConfig.discord.serverUpMessage); 307 | } 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/main/java/com/nguyenquyhy/discordbridge/utils/TextUtil.java: -------------------------------------------------------------------------------- 1 | package com.nguyenquyhy.discordbridge.utils; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.nguyenquyhy.discordbridge.DiscordBridge; 6 | import com.nguyenquyhy.discordbridge.models.ChannelMinecraftConfigCore; 7 | import com.nguyenquyhy.discordbridge.models.ChannelMinecraftEmojiConfig; 8 | import com.nguyenquyhy.discordbridge.models.ChannelMinecraftMentionConfig; 9 | import net.dv8tion.jda.core.entities.*; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.spongepowered.api.entity.living.player.Player; 13 | import org.spongepowered.api.text.Text; 14 | import org.spongepowered.api.text.action.TextActions; 15 | import org.spongepowered.api.text.format.TextColor; 16 | import org.spongepowered.api.text.format.TextColors; 17 | import org.spongepowered.api.text.format.TextStyle; 18 | import org.spongepowered.api.text.format.TextStyles; 19 | import org.spongepowered.api.text.serializer.TextSerializers; 20 | 21 | import java.awt.*; 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | /** 32 | * Created by Hy on 8/29/2016. 33 | */ 34 | public class TextUtil { 35 | private static final Pattern urlPattern = 36 | Pattern.compile("(?(^|\\s))(?(&[0-9a-flmnork])+)?(?(http(s)?://)?([A-Za-z0-9]+\\.)+[A-Za-z0-9]{2,}\\S*)", Pattern.CASE_INSENSITIVE); 37 | private static final Pattern mentionPattern = 38 | Pattern.compile("([@#]\\S*)"); 39 | 40 | public static final StyleTuple EMPTY = new StyleTuple(TextColors.NONE, TextStyles.NONE); 41 | 42 | public static String formatDiscordMessage(String message) { 43 | for (Emoji emoji : Emoji.values()) { 44 | message = message.replace(emoji.unicode, emoji.minecraftFormat); 45 | } 46 | return message; 47 | } 48 | 49 | public static String formatMinecraftMessage(String message) { 50 | for (Emoji emoji : Emoji.values()) { 51 | message = message.replace(emoji.minecraftFormat, emoji.discordFormat); 52 | } 53 | return message; 54 | } 55 | 56 | /** 57 | * @param message the Minecraft message to be checked for valid mentions 58 | * @param server the server to search through Users and Roles 59 | * @param player the player who's permissions to check 60 | * @param isBot used to ignore permission checks for authenticated users 61 | * @return the message with mentions properly formatted for Discord, if allowed 62 | */ 63 | public static String formatMinecraftMention(String message, Guild server, Player player, boolean isBot) { 64 | Matcher m = mentionPattern.matcher(message); 65 | Logger logger = DiscordBridge.getInstance().getLogger(); 66 | 67 | while (m.find()) { 68 | String mention = m.group(); 69 | if (mention.contains("@")) { 70 | String mentionName = mention.replace("@", ""); 71 | if ((mentionName.equalsIgnoreCase("here") && isBot && !player.hasPermission("discordbridge.mention.here")) || 72 | (mentionName.equalsIgnoreCase("everyone") && isBot && !player.hasPermission("discordbridge.mention.everyone"))) { 73 | message = message.replace(mention, mentionName); 74 | continue; 75 | } 76 | if (!isBot || player.hasPermission("discordbridge.mention.name." + mentionName.toLowerCase())) { 77 | Optional user = DiscordUtil.getMemberByName(mentionName, server); 78 | logger.debug(String.format("Found user %s: %s", mentionName, user.isPresent())); 79 | if (user.isPresent()) { 80 | message = message.replace(mention, "<@" + user.get().getUser().getId() + ">"); 81 | continue; 82 | } 83 | } 84 | if (!isBot || player.hasPermission("discordbridge.mention.role." + mentionName.toLowerCase())) { 85 | Optional role = DiscordUtil.getRoleByName(mentionName, server); 86 | logger.debug(String.format("Found role %s: %s", mentionName, role.isPresent())); 87 | if (role.isPresent() && role.get().isMentionable()) { 88 | message = message.replace(mention, "<@&" + role.get().getId() + ">"); 89 | } 90 | } 91 | } else if (mention.contains("#")) { 92 | String mentionName = mention.replace("#", ""); 93 | if (!isBot || player.hasPermission("discordbridge.mention.channel." + mentionName.toLowerCase())) { 94 | Optional channel = DiscordUtil.getChannelByName(mentionName, server); 95 | logger.debug(String.format("Found channel %s: %s", mentionName, channel.isPresent())); 96 | if (channel.isPresent()) { 97 | message = message.replace(mention, "<#" + channel.get().getId() + ">"); 98 | } 99 | } 100 | } 101 | } 102 | return message; 103 | } 104 | 105 | private static Map> needReplacementMap = new HashMap<>(); 106 | 107 | public static String escapeForDiscord(String text, String template, String token) { 108 | if (!needReplacementMap.containsKey(token)) { 109 | needReplacementMap.put(token, new HashMap<>()); 110 | } 111 | Map needReplacement = needReplacementMap.get(token); 112 | if (!needReplacement.containsKey(template)) { 113 | boolean need = !Pattern.matches(".*`.*" + token + ".*`.*", template) 114 | && Pattern.matches(".*_.*" + token + ".*_.*", template); 115 | needReplacement.put(template, need); 116 | } 117 | if (needReplacement.get(template)) text = text.replace("_", "\\_"); 118 | return text; 119 | } 120 | 121 | /** 122 | * @param config 123 | * @param message 124 | * @return 125 | */ 126 | public static Text formatForMinecraft(ChannelMinecraftConfigCore config, Message message) { 127 | Guild server = message.getGuild(); 128 | User author = message.getAuthor(); 129 | 130 | // Replace %u with author's username 131 | String s = ConfigUtil.get(config.chatTemplate, "&7<%a> &f%s").replace("%u", author.getName()); 132 | 133 | // Replace %n with author's nickname or username 134 | Member authorMember = server.getMember(author); 135 | String nickname = authorMember.getNickname() != null ? authorMember.getNickname() : author.getName(); 136 | s = s.replace("%a", nickname); 137 | 138 | // Get author's highest role 139 | Optional highestRole = getHighestRole(authorMember); 140 | String roleName = "Discord"; //(config.roles.containsKey("everyone")) ? config.roles.get("everyone").name : "Member"; 141 | Color roleColor = Color.WHITE; 142 | if (highestRole.isPresent()) { 143 | roleName = highestRole.get().getName(); 144 | roleColor = highestRole.get().getColor(); 145 | } 146 | // Replace %r with Message author's highest role 147 | String colorString = ColorUtil.getColorCode(roleColor); 148 | s = (StringUtils.isNotBlank(colorString)) ? s.replace("%r", colorString + roleName + "&r") : s.replace("%r", roleName); 149 | // Replace %g with Message author's game 150 | String game = authorMember.getGame().getName(); 151 | if (game != null) s = s.replace("%g", game); 152 | 153 | // Add the actual message 154 | s = String.format(s, message.getContent()); 155 | 156 | // Replace Discord-specific stuffs 157 | s = TextUtil.formatDiscordMessage(s); 158 | 159 | // Format URL 160 | List texts = formatUrl(s); 161 | // Replace user mentions with readable names 162 | texts = formatUserMentions(texts, config.mention, message.getMentionedMembers(), server); 163 | // Replace role mentions 164 | texts = formatRoleMentions(texts, config.mention, message.getMentionedRoles()); 165 | // Format @here/@everyone mentions 166 | texts = formatEveryoneMentions(texts, config.mention, message.mentionsEveryone()); 167 | // Format #channel mentions 168 | texts = formatChannelMentions(texts, config.mention); 169 | // Format custom :emjoi: 170 | texts = formatCustomEmoji(texts, config.emoji); 171 | 172 | return Text.join(texts); 173 | } 174 | 175 | /** 176 | * @param texts The message that may contain User mentions 177 | * @param config The mention config to be used for formatting 178 | * @param mentions The list of users mentioned 179 | * @param server The server to be used for nickname support 180 | * @return The final message with User mentions formatted 181 | */ 182 | private static List formatUserMentions(List texts, ChannelMinecraftMentionConfig config, List mentions, Guild server) { 183 | if (mentions.isEmpty()) return texts; 184 | // Prepare the text builders 185 | Map formattedMentioning = new HashMap<>(); 186 | for (Member mention : mentions) { 187 | Optional role = getHighestRole(mention); 188 | String nick = (mention.getNickname() != null) ? mention.getNickname() : mention.getEffectiveName(); 189 | String mentionString = ConfigUtil.get(config.userTemplate, "@%a") 190 | .replace("%s", nick).replace("%a", nick) 191 | .replace("%u", mention.getEffectiveName()); 192 | Text.Builder formatted = Text.builder().append(TextSerializers.FORMATTING_CODE.deserialize(mentionString)) 193 | .onHover(TextActions.showText(Text.of("Mentioning user " + nick + "."))); 194 | if (role.isPresent()) { 195 | formatted = formatted.color(ColorUtil.getColor(role.get().getColor())); 196 | } 197 | formattedMentioning.put(mention, formatted); 198 | } 199 | 200 | // Replace the mention 201 | for (Member mention : mentions) { 202 | String mentionString = mention.getAsMention(); 203 | texts = replaceMention(texts, mentionString, formattedMentioning.get(mention)); 204 | mentionString = mention.getUser().getAsMention(); 205 | texts = replaceMention(texts, mentionString, formattedMentioning.get(mention)); 206 | } 207 | 208 | return texts; 209 | } 210 | 211 | /** 212 | * @param texts The message that may contain Role mentions 213 | * @param config The mention config to be used for formatting 214 | * @param mentions The list of roles mentioned 215 | * @return The final message with Role mentions formatted 216 | */ 217 | private static List formatRoleMentions(List texts, ChannelMinecraftMentionConfig config, List mentions) { 218 | if (mentions.isEmpty()) return texts; 219 | 220 | // Prepare the text builders 221 | Map formattedMentioning = new HashMap<>(); 222 | for (Role mention : mentions) { 223 | String mentionString = ConfigUtil.get(config.roleTemplate, "@%s").replace("%s", mention.getName()); 224 | Text.Builder builder = Text.builder().append(TextSerializers.FORMATTING_CODE.deserialize(mentionString)) 225 | .onHover(TextActions.showText(Text.of("Mentioning role " + mention.getName() + "."))); 226 | if (mention.getColor() != null) { 227 | builder.color(ColorUtil.getColor(mention.getColor())); 228 | } 229 | formattedMentioning.put(mention, builder); 230 | } 231 | 232 | for (Role mention : mentions) { 233 | String mentionString = "<@&" + mention.getId() + ">"; 234 | texts = replaceMention(texts, mentionString, formattedMentioning.get(mention)); 235 | } 236 | 237 | return texts; 238 | } 239 | 240 | /** 241 | * @param texts The message that may contain everyone mentions 242 | * @param config The mention config to be used for formatting 243 | * @param mention Whether everyone was mentioned 244 | * @return The final message with everyone mentions formatted 245 | */ 246 | private static List formatEveryoneMentions(List texts, ChannelMinecraftMentionConfig config, boolean mention) { 247 | if (!mention) return texts; 248 | String mentionText = ConfigUtil.get(config.everyoneTemplate, "@%s").replace("%s", "everyone"); 249 | Text.Builder builder = Text.builder().append(TextSerializers.FORMATTING_CODE.deserialize(mentionText)) 250 | .onHover(TextActions.showText(Text.of("Mentioning everyone."))); 251 | texts = replaceMention(texts, "(@(everyone))", builder); 252 | 253 | mentionText = ConfigUtil.get(config.everyoneTemplate, "@%s").replace("%s", "here"); 254 | builder = Text.builder().append(TextSerializers.FORMATTING_CODE.deserialize(mentionText)) 255 | .onHover(TextActions.showText(Text.of("Mentioning online people."))); 256 | texts = replaceMention(texts, "(@(here))", builder); 257 | 258 | return texts; 259 | } 260 | 261 | private static Pattern channelPattern = Pattern.compile("<#[0-9]+>"); 262 | 263 | /** 264 | * @param texts The message that may contain channel mentions 265 | * @param config The mention config to be used for formatting 266 | * @return The final message with channel mentions formatted 267 | */ 268 | private static List formatChannelMentions(List texts, ChannelMinecraftMentionConfig config) { 269 | Map formattedMentions = new HashMap<>(); 270 | for (Text text : texts) { 271 | String serialized = TextSerializers.FORMATTING_CODE.serialize(text); 272 | Matcher matcher = channelPattern.matcher(serialized); 273 | while (matcher.find()) { 274 | String channelId = serialized.substring(matcher.start() + 2, matcher.end() - 1); 275 | if (!formattedMentions.containsKey(channelId)) { 276 | Channel channel = DiscordBridge.getInstance().getBotClient().getTextChannelById(channelId); 277 | String mentionText = ConfigUtil.get(config.channelTemplate, "#%s").replace("%s", channel.getName()); 278 | Text.Builder builder = Text.builder().append(TextSerializers.FORMATTING_CODE.deserialize(mentionText)) 279 | .onHover(TextActions.showText(Text.of("Mentioning channel " + channel.getName() + "."))); 280 | formattedMentions.put(channelId, builder); 281 | } 282 | } 283 | } 284 | 285 | for (String channelId : formattedMentions.keySet()) { 286 | texts = replaceMention(texts, "<#" + channelId + ">", formattedMentions.get(channelId)); 287 | } 288 | return texts; 289 | } 290 | 291 | private static Pattern customEmoji = Pattern.compile("<:([a-z]+):([0-9]+)>", Pattern.CASE_INSENSITIVE); 292 | 293 | /** 294 | * @param texts The message that may contain custom emoji 295 | * @param config The mention config to be used for formatting 296 | * @return The final message with custom emoji formatted 297 | */ 298 | private static List formatCustomEmoji(List texts, ChannelMinecraftEmojiConfig config) { 299 | for(Text text : texts){ 300 | String serialized = TextSerializers.FORMATTING_CODE.serialize(text); 301 | Matcher matcher = customEmoji.matcher(serialized); 302 | while (matcher.find()) { 303 | String name = matcher.group(1); 304 | String id = matcher.group(2); 305 | 306 | Text.Builder builder = TextSerializers.FORMATTING_CODE.deserialize(config.template.replace("%n", name)).toBuilder(); 307 | 308 | if (config.allowLink) { 309 | try { 310 | builder = builder.onClick(TextActions.openUrl(new URL("https://cdn.discordapp.com/emojis/" + id + ".png"))); 311 | } catch (MalformedURLException ignored) { } 312 | } 313 | 314 | if (StringUtils.isNotBlank(config.hoverTemplate)) 315 | builder = builder.onHover(TextActions.showText(Text.of(config.hoverTemplate))); 316 | texts = replaceMention(texts, "<:"+name+":"+id+">", builder); 317 | } 318 | } 319 | return texts; 320 | } 321 | 322 | public static List formatUrl(String message) { 323 | Preconditions.checkNotNull(message, "message"); 324 | List texts = Lists.newArrayList(); 325 | if (message.isEmpty()) { 326 | texts.add(Text.EMPTY); 327 | return texts; 328 | } 329 | 330 | Matcher m = urlPattern.matcher(message); 331 | if (!m.find()) { 332 | texts.add(TextSerializers.FORMATTING_CODE.deserialize(message)); 333 | return texts; 334 | } 335 | 336 | String remaining = message; 337 | StyleTuple st = EMPTY; 338 | do { 339 | 340 | // We found a URL. We split on the URL that we have. 341 | String[] textArray = remaining.split(urlPattern.pattern(), 2); 342 | Text first = Text.builder().color(st.colour).style(st.style) 343 | .append(TextSerializers.FORMATTING_CODE.deserialize(textArray[0])).build(); 344 | 345 | // Add this text to the list regardless. 346 | texts.add(first); 347 | 348 | // If we have more to do, shove it into the "remaining" variable. 349 | if (textArray.length == 2) { 350 | remaining = textArray[1]; 351 | } else { 352 | remaining = null; 353 | } 354 | 355 | // Get the last colour & styles 356 | String colourMatch = m.group("colour"); 357 | if (colourMatch != null && !colourMatch.isEmpty()) { 358 | first = TextSerializers.FORMATTING_CODE.deserialize(m.group("colour") + " "); 359 | } 360 | 361 | st = getLastColourAndStyle(first, st); 362 | 363 | // Build the URL 364 | String url = m.group("url"); 365 | String toParse = TextSerializers.FORMATTING_CODE.stripCodes(url); 366 | String whiteSpace = m.group("first"); 367 | texts.add(Text.of(whiteSpace)); 368 | 369 | try { 370 | URL urlObj; 371 | if (!toParse.startsWith("http://") && !toParse.startsWith("https://")) { 372 | urlObj = new URL("http://" + toParse); 373 | } else { 374 | urlObj = new URL(toParse); 375 | } 376 | 377 | texts.add(Text.builder(url).color(TextColors.DARK_AQUA).style(TextStyles.UNDERLINE) 378 | .onHover(TextActions.showText(Text.of("Click to open " + url))) 379 | .onClick(TextActions.openUrl(urlObj)) 380 | .build()); 381 | } catch (MalformedURLException e) { 382 | // URL parsing failed, just put the original text in here. 383 | DiscordBridge.getInstance().getLogger().warn("Malform: " + url); 384 | texts.add(Text.builder(url).color(st.colour).style(st.style).build()); 385 | } 386 | } while (remaining != null && m.find()); 387 | 388 | // Add the last bit. 389 | if (remaining != null) { 390 | texts.add(Text.builder().color(st.colour).style(st.style) 391 | .append(TextSerializers.FORMATTING_CODE.deserialize(remaining)).build()); 392 | } 393 | 394 | return texts; 395 | } 396 | 397 | private static List replaceMention(List texts, String mentionString, Text.Builder mentionBuilder) { 398 | List result = Lists.newArrayList(); 399 | StyleTuple st = EMPTY; 400 | for (Text text : texts) { 401 | Text remaining = text; 402 | while (remaining != null) { 403 | String serialized = TextSerializers.FORMATTING_CODE.serialize(remaining); 404 | String[] splitted = serialized.split(mentionString, 2); 405 | if (splitted.length == 2) { 406 | // Add first part 407 | Text first = TextSerializers.FORMATTING_CODE.deserialize(splitted[0]); 408 | result.add(first); 409 | 410 | // Add the mention 411 | result.add(mentionBuilder.build()); 412 | 413 | // Calculate the remaining 414 | st = TextUtil.getLastColourAndStyle(first, st); 415 | remaining = Text.builder().color(st.colour).style(st.style) 416 | .append(TextSerializers.FORMATTING_CODE.deserialize(splitted[1])).build(); 417 | } else { 418 | result.add(remaining); 419 | break; 420 | } 421 | } 422 | } 423 | return result; 424 | } 425 | 426 | private static StyleTuple getLastColourAndStyle(Text text, StyleTuple current) { 427 | List texts = flatten(text); 428 | TextColor tc = TextColors.NONE; 429 | TextStyle ts = TextStyles.NONE; 430 | for (int i = texts.size() - 1; i > -1; i--) { 431 | // If we have both a Text Colour and a Text Style, then break out. 432 | if (tc != TextColors.NONE && ts != TextStyles.NONE) { 433 | break; 434 | } 435 | 436 | if (tc == TextColors.NONE) { 437 | tc = texts.get(i).getColor(); 438 | } 439 | 440 | if (ts == TextStyles.NONE) { 441 | ts = texts.get(i).getStyle(); 442 | } 443 | } 444 | 445 | if (current == null) { 446 | return new StyleTuple(tc, ts); 447 | } 448 | 449 | return new StyleTuple(tc != TextColors.NONE ? tc : current.colour, ts != TextStyles.NONE ? ts : current.style); 450 | } 451 | 452 | private static List flatten(Text text) { 453 | List texts = Lists.newArrayList(text); 454 | if (!text.getChildren().isEmpty()) { 455 | text.getChildren().forEach(x -> texts.addAll(flatten(x))); 456 | } 457 | 458 | return texts; 459 | } 460 | 461 | /** 462 | * @param user 463 | * @return 464 | */ 465 | public static Optional getHighestRole(Member user) { 466 | int position = 0; 467 | Optional highestRole = Optional.empty(); 468 | for (Role role : user.getRoles()) { 469 | if (role.getPosition() > position) { 470 | position = role.getPosition(); 471 | highestRole = Optional.of(role); 472 | } 473 | } 474 | return highestRole; 475 | } 476 | 477 | private static final class StyleTuple { 478 | final TextColor colour; 479 | final TextStyle style; 480 | 481 | StyleTuple(TextColor colour, TextStyle style) { 482 | this.colour = colour; 483 | this.style = style; 484 | } 485 | } 486 | } 487 | --------------------------------------------------------------------------------