Skip to content

Commit

Permalink
Send formatted messages to the Minecraft server
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Syntro42 committed Dec 23, 2023
1 parent 429dfe2 commit 8adbc38
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 2 deletions.
47 changes: 47 additions & 0 deletions src/lib/messages.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion src/minecraftServer.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -17,6 +18,7 @@ export class MinecraftServer {

async sendMessage(message: Message): Promise<void> {
// 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}`);
}
}
93 changes: 93 additions & 0 deletions tests/messages.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
6 changes: 5 additions & 1 deletion tests/minecraftServer.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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");
});
});

0 comments on commit 8adbc38

Please sign in to comment.