From 8adbc38fe2902b463314976bbdfb28971769f306 Mon Sep 17 00:00:00 2001 From: Syntro42 Date: Fri, 22 Dec 2023 18:05:42 -0600 Subject: [PATCH] Send formatted messages to the Minecraft server All messages are sent as `[user] message`. The user text will be colored in Minecraft with the hex color of the Discord user's highest role. If there are @ pings or # channel declarations in the Discord message, they will be translated to text before being sent to Minecraft so it isn't some weird Discord ID in the chat. --- src/lib/messages.ts | 47 ++++++++++++++++++ src/minecraftServer.ts | 4 +- tests/messages.test.ts | 93 +++++++++++++++++++++++++++++++++++ tests/minecraftServer.test.ts | 6 ++- 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/lib/messages.ts create mode 100644 tests/messages.test.ts diff --git a/src/lib/messages.ts b/src/lib/messages.ts new file mode 100644 index 0000000..b854d76 --- /dev/null +++ b/src/lib/messages.ts @@ -0,0 +1,47 @@ +import { Message } from "discord.js"; + +export class MinecraftMessage { + static format(message: Message): string { + const userColor = message.member!.roles.highest.hexColor; + const userName = { text: message.member!.displayName, color: userColor }; + const messageContent = this.formatContent(message); + const text = ["[", userName, "] ", messageContent]; + return JSON.stringify(text); + } + + private static formatContent(message: Message): string { + let content = message.content; + if (content.includes("<@")) content = this.formatUsersAndRoles(message, content); + if (content.includes("<#")) content = this.formatChannels(message, content); + return content; + } + + private static formatUsersAndRoles(message: Message, content: string): string { + const members = message.guild!.members.cache; + members.forEach((member) => { + if (content.includes(member.id)) { + content = content.replaceAll("<@" + member.id + ">", "@" + member.displayName); + } + }); + + const roles = message.guild!.roles.cache; + roles.forEach((role) => { + if (content.includes(role.id)) { + content = content.replaceAll("<@&" + role.id + ">", "@" + role.name); + } + }); + + return content; + } + + private static formatChannels(message: Message, content: string): string { + const channels = message.guild!.channels.cache; + channels.forEach((channel) => { + if (content.includes(channel.id)) { + content = content.replaceAll("<#" + channel.id + ">", "#" + channel.name); + } + }); + + return content; + } +} diff --git a/src/minecraftServer.ts b/src/minecraftServer.ts index f4d7052..29221c1 100644 --- a/src/minecraftServer.ts +++ b/src/minecraftServer.ts @@ -1,5 +1,6 @@ import { IServer } from "./interfaces.js"; import { Message } from "discord.js"; +import { MinecraftMessage } from "./lib/messages.js"; import { Rcon } from "./lib/rcon/rcon.js"; export class MinecraftServer { @@ -17,6 +18,7 @@ export class MinecraftServer { async sendMessage(message: Message): Promise { // TODO: Handle errors in send - await this.rcon.send(`tellraw @a {"text": "${message.content}"}`); + const minecraftMsg = MinecraftMessage.format(message); + await this.rcon.send(`tellraw @a ${minecraftMsg}`); } } diff --git a/tests/messages.test.ts b/tests/messages.test.ts new file mode 100644 index 0000000..8b7acfc --- /dev/null +++ b/tests/messages.test.ts @@ -0,0 +1,93 @@ +import { Message } from "discord.js"; +import { MinecraftMessage } from "../src/lib/messages.js"; +import { mockMember } from "./mockHelpers.js"; + +describe("MinecraftMessage class", () => { + const member = mockMember(); + member.roles.highest = { hexColor: "#000000" }; + + const members = { + cache: [ + { id: "10", displayName: "user1" }, + { id: "11", displayName: "user2" }, + ], + }; + const roles = { + cache: [ + { id: "10", name: "role1" }, + { id: "11", name: "role2" }, + ], + }; + const channels = { + cache: [ + { id: "10", name: "channel1" }, + { id: "11", name: "channel2" }, + ], + }; + + it("Formats a simple message correctly", () => { + const mockMessage = { content: "simple", member: member } as Message; + const msg = MinecraftMessage.format(mockMessage); + + expect(msg).toContain("simple"); + expect(msg).toContain(member.roles.highest.hexColor); + }); + + it("Formats a message with user", () => { + const mockMessage = { + content: "channel <@11>", + guild: { + members: members, + roles: { cache: [] }, + }, + member: member, + } as Message; + const msg = MinecraftMessage.format(mockMessage); + + expect(msg).toContain("channel @user2"); + }); + + it("Formats a message with role", () => { + const mockMessage = { + content: "channel <@&11>", + guild: { + members: { cache: [] }, + roles: roles, + }, + member: member, + } as Message; + const msg = MinecraftMessage.format(mockMessage); + + expect(msg).toContain("channel @role2"); + }); + + it("Formats a message with channel", () => { + const mockMessage = { + content: "channel <#11>", + guild: { + channels: channels, + }, + member: member, + } as Message; + const msg = MinecraftMessage.format(mockMessage); + + expect(msg).toContain("channel #channel2"); + }); + + it("Formats a complex message", () => { + const mockMessage = { + content: "channel <#11> user <@10> role <@&10> channel <#11> user <@11> role <@&10>", + guild: { + channels: channels, + members: members, + roles: roles, + }, + member: member, + } as Message; + const msg = MinecraftMessage.format(mockMessage); + + expect(msg).toContain( + "channel #channel2 user @user1 role @role1 channel #channel2 user @user2 role @role1", + ); + }); +}); diff --git a/tests/minecraftServer.test.ts b/tests/minecraftServer.test.ts index 54cce44..ad92693 100644 --- a/tests/minecraftServer.test.ts +++ b/tests/minecraftServer.test.ts @@ -1,4 +1,5 @@ import { Message } from "discord.js"; +import { MinecraftMessage } from "../src/lib/messages.js"; import { MinecraftServer } from "../src/minecraftServer.js"; import { readConfig } from "./mockHelpers.js"; @@ -14,9 +15,12 @@ describe("MinecraftServer class", () => { it("Sends a message through RCON", () => { const mockMessage = { content: "test" } as Message; + jest.spyOn(MinecraftMessage, "format").mockImplementationOnce(() => { + return "format test"; + }); const mockSend = jest.spyOn(server.rcon, "send").mockImplementation(); server.sendMessage(mockMessage); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockSend.mock.calls[0][0]).toContain(mockMessage.content); + expect(mockSend.mock.calls[0][0]).toContain("format test"); }); });