diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew index b740cf1..f5feea6 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/settings.gradle b/settings.gradle index ec9681d..fe8f1d8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,5 +17,5 @@ pluginManagement { } plugins { - id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.27' + id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.29' } diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..d5c5f5f --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml new file mode 100644 index 0000000..54a0481 --- /dev/null +++ b/src/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/com/myname/mymodid/ClientProxy.java b/src/main/java/moe/yuyui/mcstreamerbot/ClientProxy.java similarity index 87% rename from src/main/java/com/myname/mymodid/ClientProxy.java rename to src/main/java/moe/yuyui/mcstreamerbot/ClientProxy.java index ea70aa1..dfd2795 100644 --- a/src/main/java/com/myname/mymodid/ClientProxy.java +++ b/src/main/java/moe/yuyui/mcstreamerbot/ClientProxy.java @@ -1,4 +1,4 @@ -package com.myname.mymodid; +package moe.yuyui.mcstreamerbot; public class ClientProxy extends CommonProxy { diff --git a/src/main/java/com/myname/mymodid/CommonProxy.java b/src/main/java/moe/yuyui/mcstreamerbot/CommonProxy.java similarity index 61% rename from src/main/java/com/myname/mymodid/CommonProxy.java rename to src/main/java/moe/yuyui/mcstreamerbot/CommonProxy.java index dfdfe53..a11fac6 100644 --- a/src/main/java/com/myname/mymodid/CommonProxy.java +++ b/src/main/java/moe/yuyui/mcstreamerbot/CommonProxy.java @@ -1,9 +1,13 @@ -package com.myname.mymodid; +package moe.yuyui.mcstreamerbot; + +import net.minecraftforge.common.MinecraftForge; import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLPostInitializationEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.event.FMLServerStartingEvent; +import moe.yuyui.mcstreamerbot.events.minecraft.OnWorldJoin; +import moe.yuyui.mcstreamerbot.events.minecraft.OnWorldLeave; public class CommonProxy { @@ -12,12 +16,16 @@ public class CommonProxy { public void preInit(FMLPreInitializationEvent event) { Config.synchronizeConfiguration(event.getSuggestedConfigurationFile()); - MyMod.LOG.info(Config.greeting); - MyMod.LOG.info("I am MyMod at version " + Tags.VERSION); + MinecraftStreamerBotIntegration.LOG.info(Config.greeting); + MinecraftStreamerBotIntegration.LOG.info("I am MyMod at version " + Tags.VERSION); } // load "Do your mod setup. Build whatever data structures you care about. Register recipes." (Remove if not needed) - public void init(FMLInitializationEvent event) {} + public void init(FMLInitializationEvent event) { + MinecraftForge.EVENT_BUS.register(new OnWorldJoin()); + MinecraftForge.EVENT_BUS.register(new OnWorldLeave()); + MinecraftStreamerBotIntegration.LOG.info("Loaded MCStreamerBot"); + } // postInit "Handle interaction with other mods, complete your setup based on this." (Remove if not needed) public void postInit(FMLPostInitializationEvent event) {} diff --git a/src/main/java/com/myname/mymodid/Config.java b/src/main/java/moe/yuyui/mcstreamerbot/Config.java similarity index 93% rename from src/main/java/com/myname/mymodid/Config.java rename to src/main/java/moe/yuyui/mcstreamerbot/Config.java index b1c4f50..1a4a366 100644 --- a/src/main/java/com/myname/mymodid/Config.java +++ b/src/main/java/moe/yuyui/mcstreamerbot/Config.java @@ -1,4 +1,4 @@ -package com.myname.mymodid; +package moe.yuyui.mcstreamerbot; import java.io.File; diff --git a/src/main/java/com/myname/mymodid/MyMod.java b/src/main/java/moe/yuyui/mcstreamerbot/MinecraftStreamerBotIntegration.java similarity index 74% rename from src/main/java/com/myname/mymodid/MyMod.java rename to src/main/java/moe/yuyui/mcstreamerbot/MinecraftStreamerBotIntegration.java index b6f6735..20c45d3 100644 --- a/src/main/java/com/myname/mymodid/MyMod.java +++ b/src/main/java/moe/yuyui/mcstreamerbot/MinecraftStreamerBotIntegration.java @@ -1,4 +1,4 @@ -package com.myname.mymodid; +package moe.yuyui.mcstreamerbot; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -9,14 +9,21 @@ import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLPostInitializationEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.event.FMLServerStartingEvent; +import moe.yuyui.mcstreamerbot.common.EventListener; -@Mod(modid = MyMod.MODID, version = Tags.VERSION, name = "MyMod", acceptedMinecraftVersions = "[1.7.10]") -public class MyMod { +@Mod( + modid = MinecraftStreamerBotIntegration.MODID, + version = Tags.VERSION, + name = "MyMod", + acceptedMinecraftVersions = "[1.7.10]") +public class MinecraftStreamerBotIntegration { - public static final String MODID = "mymodid"; + public static final String MODID = "mcstreamerbot"; public static final Logger LOG = LogManager.getLogger(MODID); - @SidedProxy(clientSide = "com.myname.mymodid.ClientProxy", serverSide = "com.myname.mymodid.CommonProxy") + public static EventListener eventListener; + + @SidedProxy(clientSide = "moe.yuyui.mcstreamerbot.ClientProxy", serverSide = "moe.yuyui.mcstreamerbot.CommonProxy") public static CommonProxy proxy; @Mod.EventHandler diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/EventListener.java b/src/main/java/moe/yuyui/mcstreamerbot/common/EventListener.java new file mode 100644 index 0000000..021cdaf --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/EventListener.java @@ -0,0 +1,27 @@ +package moe.yuyui.mcstreamerbot.common; + +public class EventListener extends Thread { + + private final WSServer _websocketServer; + + public EventListener(WSServer websocketServer) { + this._websocketServer = websocketServer; + start(); + } + + @Override + public void run() { + try { + this._websocketServer.run(); + System.out.println("Event Listener started"); + } catch (NullPointerException e) { + System.out.println("Event Listener stopped. Websocket has failed."); + } + } + + public void end() throws InterruptedException { + this._websocketServer.getConnections() + .forEach((connection) -> connection.closeConnection(1, "Server closed")); + this._websocketServer.stop(); + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/WSServer.java b/src/main/java/moe/yuyui/mcstreamerbot/common/WSServer.java new file mode 100644 index 0000000..c3f2686 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/WSServer.java @@ -0,0 +1,138 @@ +package moe.yuyui.mcstreamerbot.common; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipException; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import moe.yuyui.mcstreamerbot.common.beans.MessagePayloadBean; +import moe.yuyui.mcstreamerbot.common.enums.MessageType; +import moe.yuyui.mcstreamerbot.events.custom.OnChatMessage; + +public class WSServer extends WebSocketServer { + + private final Set _authClients = new HashSet<>(); + private byte[] _authToken = null; + + public WSServer(InetSocketAddress address) { + super(address); + } + + public WSServer(InetSocketAddress address, String authToken) throws NoSuchAlgorithmException { + super(address); + this._authToken = MessageDigest.getInstance("SHA-256") + .digest(authToken.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + if (_authToken != null) { + conn.send("bad girl"); + return; + } + _authClients.add(conn); + System.out.println("New connection"); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + _authClients.remove(conn); + System.out.println("Connection closed"); + } + + @Override + public void onMessage(WebSocket conn, String message) {} + + private boolean isAuthenticated(WebSocket conn) { + return _authClients.contains(conn); + } + + private boolean isBearerValid(String token) { + if (_authToken == null) { + return true; + } + if (token == null) { + return false; + } + return token.equals(new String(_authToken, StandardCharsets.UTF_8)); + } + + public String decompressMessage(ByteBuffer byteBuffer) { + if (byteBuffer == null) { + return null; + } + try { + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(byteBuffer.array())); + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + return builder.toString(); + } catch (ZipException e) { + System.out.println("Message is not valid"); + } catch (IOException ignored) {} + return null; + + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer byteBuffer) { + String message = decompressMessage(byteBuffer); + if (message == null) { + return; + } + + Gson gson = new GsonBuilder().serializeNulls() + .create(); + MessagePayloadBean payload = gson.fromJson(message, MessagePayloadBean.class); + + if (!isAuthenticated(conn) || isBearerValid(payload.getToken())) { + _authClients.add(conn); + } + switch (MessageType.valueOf(payload.getType())) { + case CHAT: { + OnChatMessage.sendMessage(payload.getParts(), payload.getTimestamp(), payload.getUser()); + break; + } + case COMMAND: { + break; + } + case LISTEN: { + break; + } + case IGNORE: { + break; + } + } + } + + @Override + public void onError(WebSocket conn, Exception ex) { + ex.printStackTrace(); + conn.close(); + } + + @Override + public void onStart() { + System.out.println("Server started"); + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/beans/EmojiBean.java b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/EmojiBean.java new file mode 100644 index 0000000..007972b --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/EmojiBean.java @@ -0,0 +1,45 @@ +package moe.yuyui.mcstreamerbot.common.beans; + +import com.google.gson.annotations.SerializedName; + +public class EmojiBean { + + @SerializedName("Emoji") + private final String _emoji; + @SerializedName("Text") + private final String _text; + @SerializedName("Image") + private final String _imagePath; + @SerializedName("StartIndex") + private final int _startIndex; + @SerializedName("EndIndex") + private final int _endIndex; + + public EmojiBean(String emoji, String text, String imagePath, int startIndex, int endIndex) { + this._emoji = emoji; + this._text = text; + this._imagePath = imagePath; + this._startIndex = startIndex; + this._endIndex = endIndex; + } + + public String getEmoji() { + return this._emoji; + } + + public String getText() { + return this._text; + } + + public String getImagePath() { + return this._imagePath; + } + + public int getStartIndex() { + return this._startIndex; + } + + public int getEndIndex() { + return this._endIndex; + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessageBean.java b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessageBean.java new file mode 100644 index 0000000..805867b --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessageBean.java @@ -0,0 +1,26 @@ +package moe.yuyui.mcstreamerbot.common.beans; + +import javax.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +public class MessageBean { + + @SerializedName("Text") + private final String _text; + @SerializedName("Emoji") + private final EmojiBean _emoji; + + public MessageBean(String text, @Nullable EmojiBean emoji) { + this._text = text; + this._emoji = emoji; + } + + public String getText() { + return this._text; + } + + public EmojiBean getEmoji() { + return this._emoji; + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessagePayloadBean.java b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessagePayloadBean.java new file mode 100644 index 0000000..cb4b927 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/MessagePayloadBean.java @@ -0,0 +1,48 @@ +package moe.yuyui.mcstreamerbot.common.beans; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public class MessagePayloadBean { + + @SerializedName("Type") + private final short _type; + @SerializedName("BearerToken") + private final String _token; + @SerializedName("User") + private final UserBean _user; + @SerializedName("Parts") + private final List _parts; + @SerializedName("Timestamp") + private final long _timestamp; + + public MessagePayloadBean(short type, String token, UserBean user, List parts, long timestamp) { + this._type = type; + this._token = token; + this._user = user; + this._parts = parts; + this._timestamp = timestamp; + } + + public short getType() { + return this._type; + } + + public String getToken() { + return this._token; + } + + public UserBean getUser() { + return this._user; + } + + public List getParts() { + return this._parts; + } + + public long getTimestamp() { + return this._timestamp; + } + +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/beans/UserBean.java b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/UserBean.java new file mode 100644 index 0000000..f9bfd77 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/beans/UserBean.java @@ -0,0 +1,46 @@ +package moe.yuyui.mcstreamerbot.common.beans; + +import com.google.gson.annotations.SerializedName; + +public class UserBean { + + @SerializedName("Name") + private final String _name; + @SerializedName("YoutubeId") + private final String _youtubeId; + @SerializedName("IsSubscriber") + private final boolean _isSubscriber; + @SerializedName("IsModerator") + private final boolean _isModerator; + @SerializedName("IsMember") + private final boolean _isMember; + + public UserBean(String name, String youtubeId, boolean isSubscriber, boolean isModerator, boolean isMember) { + this._name = name; + this._youtubeId = youtubeId; + this._isSubscriber = isSubscriber; + this._isModerator = isModerator; + this._isMember = isMember; + } + + public String getName() { + return _name; + } + + public String getYoutubeId() { + return _youtubeId; + } + + public boolean getIsSubscriber() { + return _isSubscriber; + } + + public boolean getIsModerator() { + return _isModerator; + } + + public boolean getIsMember() { + return _isMember; + } + +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/common/enums/MessageType.java b/src/main/java/moe/yuyui/mcstreamerbot/common/enums/MessageType.java new file mode 100644 index 0000000..1e9ed71 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/common/enums/MessageType.java @@ -0,0 +1,33 @@ +package moe.yuyui.mcstreamerbot.common.enums; + +import java.util.HashMap; +import java.util.Map; + +public enum MessageType { + + CHAT(0), + COMMAND(1), + LISTEN(2), + IGNORE(3); + + private final int value; + private static final Map map = new HashMap<>(); + + MessageType(int value) { + this.value = value; + } + + static { + for (MessageType messageType : MessageType.values()) { + map.put(messageType.value, messageType); + } + } + + public static MessageType valueOf(int value) { + return (MessageType) map.get(value); + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/events/custom/OnChatMessage.java b/src/main/java/moe/yuyui/mcstreamerbot/events/custom/OnChatMessage.java new file mode 100644 index 0000000..dc767df --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/events/custom/OnChatMessage.java @@ -0,0 +1,77 @@ +package moe.yuyui.mcstreamerbot.events.custom; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; + +import moe.yuyui.mcstreamerbot.common.beans.MessageBean; +import moe.yuyui.mcstreamerbot.common.beans.UserBean; + +public final class OnChatMessage { + + private static final String _messageFormat = "<%s> %s%s[%s]%s: %s"; + + private static String buildMessage(List messageParts) { + StringBuilder finalMessage = new StringBuilder(); + for (MessageBean messagePart : messageParts) { + if (messagePart.getEmoji() != null) { + finalMessage.append( + messagePart.getEmoji() + .getText(), + messagePart.getEmoji() + .getStartIndex(), + messagePart.getEmoji() + .getEndIndex()); + } + finalMessage.append(messagePart.getText()); + } + return finalMessage.toString(); + } + + private static String getUserColor(UserBean user) { + if (user.getIsModerator()) { + return EnumChatFormatting.RED.toString(); + } + if (user.getIsMember()) { + return EnumChatFormatting.DARK_GREEN.toString(); + } + return EnumChatFormatting.RESET.toString(); + } + + private static char getUserBadge(UserBean user) { + if (user.getIsModerator()) { + return '⚔'; + } + if (user.getIsMember()) { + return '✨'; + } + return ' '; + } + + public static void sendMessage(List messageParts, long timestamp, UserBean user) { + if (messageParts == null) { + return; + } + final String message = buildMessage(messageParts); + final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("hh:mm:ss a"); + final String formattedTime = Instant.ofEpochSecond(timestamp) + .atZone(ZoneId.systemDefault()) + .format(dateFormatter); + ChatComponentText chatComponentText = new ChatComponentText( + String.format( + _messageFormat, + formattedTime, + getUserColor(user), + getUserBadge(user), + user.getName(), + EnumChatFormatting.RESET.toString(), + message.replace('§', ' '))); + Minecraft.getMinecraft().ingameGUI.getChatGUI() + .printChatMessage(chatComponentText); + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldJoin.java b/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldJoin.java new file mode 100644 index 0000000..0aaa0c9 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldJoin.java @@ -0,0 +1,28 @@ +package moe.yuyui.mcstreamerbot.events.minecraft; + +import java.net.InetSocketAddress; + +import net.minecraft.client.Minecraft; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import moe.yuyui.mcstreamerbot.MinecraftStreamerBotIntegration; +import moe.yuyui.mcstreamerbot.common.EventListener; +import moe.yuyui.mcstreamerbot.common.WSServer; + +public class OnWorldJoin { + + @SubscribeEvent + public void onEntityJoinWorld(EntityJoinWorldEvent event) { + if (event.entity == Minecraft.getMinecraft().thePlayer && Minecraft.getMinecraft() + .isIntegratedServerRunning() && event.world.isRemote) { + if (MinecraftStreamerBotIntegration.eventListener != null) { + if (MinecraftStreamerBotIntegration.eventListener.isAlive()) { + return; + } + } + MinecraftStreamerBotIntegration.eventListener = new EventListener( + new WSServer(new InetSocketAddress("127.0.0.1", 8080))); + } + } +} diff --git a/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldLeave.java b/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldLeave.java new file mode 100644 index 0000000..b0ccd08 --- /dev/null +++ b/src/main/java/moe/yuyui/mcstreamerbot/events/minecraft/OnWorldLeave.java @@ -0,0 +1,17 @@ +package moe.yuyui.mcstreamerbot.events.minecraft; + +import net.minecraftforge.event.world.WorldEvent; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import moe.yuyui.mcstreamerbot.MinecraftStreamerBotIntegration; + +public class OnWorldLeave { + + @SubscribeEvent + public void onPlayerLeave(WorldEvent.Unload event) { + try { + MinecraftStreamerBotIntegration.eventListener.end(); + } catch (InterruptedException | NullPointerException ignored) {} + + } +}