Skip to content

Commit e5cd574

Browse files
authored
The bot is in a finished state (I think)
1 parent 5d8178d commit e5cd574

14 files changed

Lines changed: 900 additions & 279 deletions

File tree

README.md

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
# hackmud-chat-client
2-
## Setup
3-
Create a discord bot using [Discord Developer Portal](https://discord.com/developers/)
4-
Create a discord server
2+
## First time setup
3+
Create a discord bot using [Discord Developer Portal](https://discord.com/developers/)
4+
Create a discord server (Its not recommended to have this bot in a public server)
55

6-
Copy (or rename) the example config "configex.json" to "config.json"
7-
Copy the bot's discord token to "DCTOKEN" (Application > Bot > Token)
8-
Copy the application's clientID to "DCCLIENTID" (Application > OAuth > ClientID)
9-
Copy the guildID to "DCGUILDID" (Turn on dev mode > Right Click guild > CopyID)
6+
### Bot Permissions
7+
**Make sure the bot has these permissons set, otherwise it will break!**
8+
```
9+
Privileged Gateway Intents
10+
- Message Content Intent
1011
11-
Run the BOT
12+
Server Role Permissions
13+
- View Channels
14+
- Manage Channels
15+
- Send Messages
16+
- Add Reactions
17+
- Manage Messages
18+
```
1219

13-
In hackmud run the command chat_pass and copy the result
14-
In discord send /auth password:"pass"
15-
Once it says its successfully set the Token run /setup
16-
The guild you have setup should now have 2 categorys (chat and tell) with channels you have joined being populated under chat and filled with messages :D (If message history doesn't show up leave and rejoin the chat ingame then try again.)
20+
Copy (or rename) the example config "configex.json" to "config.json"
21+
Copy the bot's discord token to "token" (Application > Bot > Token)
22+
Copy the application's clientID to "clientId" (Application > OAuth > ClientID)
23+
Copy the guildID to "guildId" (Turn on dev mode > Right Click guild > CopyID)
24+
Run `node deploy-commands.js` so all the commands deploy instantly to your server
25+
Run the BOT using `node index.js`
26+
27+
In hackmud run the command chat_pass and copy the result
28+
In discord send `/settings auth password:"pass"`
29+
Once it says its successfully set the Token run `/settomgs setup` then `/client start`
30+
The guild the bot is in should now have all of your users set as channels and it should start pulling messages.
31+
32+
## Contributions
33+
All contributions are greatly appriciated! I am not great at coding so expect lots of spaghetti code.
34+
35+
36+
Useful Links
37+
[Hackmud Chat API Documentation](https://hackmud.com/forums/general_discussion/chat_api_documentation)
38+
[Discord ACSI Color Codes](https://gist.github.com/kkrypt0nn/a02506f3712ff2d1c8ca7c9e0aed7c06)
39+
[Discord.JS Guide](https://discordjs.guide/)

backend/pullInterval.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
let chatPullInterval = null;
2+
3+
module.exports = {
4+
getChatPullInterval: () => chatPullInterval,
5+
setChatPullInterval: (interval) => {
6+
chatPullInterval = interval;
7+
},
8+
clearChatPullInterval: () => {
9+
if (chatPullInterval) {
10+
clearInterval(chatPullInterval);
11+
chatPullInterval = null;
12+
}
13+
},
14+
};

commands/mud/auth.js

Lines changed: 0 additions & 58 deletions
This file was deleted.

commands/mud/client.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
const { SlashCommandBuilder } = require('discord.js');
2+
const fetch = require('node-fetch');
3+
const fs = require('node:fs');
4+
const { setChatPullInterval, clearChatPullInterval, getChatPullInterval } = require('./../../backend/pullInterval');
5+
const path = require('path');
6+
const { readFile } = require('fs/promises');
7+
8+
// Full Hackmud-to-Discord color mapping //  = U+001B
9+
const hackmudToDiscordColors = {
10+
'reset': '\', // Reset text formatting
11+
'0': '', // Hackmud: #9B9B9B | Discord: Gray
12+
'1': '', // Hackmud: #FFFFFF | Discord: White
13+
'2': '', // Hackmud: #1EFF00 | Discord: Green
14+
'3': '', // Hackmud: #0070DD | Discord: Blue
15+
'4': '', // Hackmud: #B035EE | Discord: Pink
16+
'5': '', // Hackmud: #FF8000 | Discord: Yellow
17+
'6': '', // Hackmud: #FF8000 | Discord: Yellow
18+
'7': '', // Hackmud: #FF8000 | Discord: Yellow
19+
'8': '', // Hackmud: #FF8000 | Discord: Yellow
20+
'9': '', // Hackmud: #FF8000 | Discord: Yellow
21+
'a': '', // Hackmud: #000000 | Discord: Gray
22+
'b': '', // Hackmud: #3F3F3F | Discord: Gray
23+
'c': '', // Hackmud: #676767 | Discord: Gray
24+
'd': '', // Hackmud: #7D0000 | Discord: Red
25+
'e': '', // Hackmud: #8E3434 | Discord: Red
26+
'f': '', // Hackmud: #A34F00 | Discord: Yellow
27+
'g': '', // Hackmud: #725437 | Discord: Yellow
28+
'h': '', // Hackmud: #A88600 | Discord: Yellow
29+
'i': '', // Hackmud: #B2934A | Discord: Yellow
30+
'j': '', // Hackmud: #939500 | Discord: Green
31+
'k': '', // Hackmud: #495225 | Discord: Green
32+
'l': '', // Hackmud: #299400 | Discord: Green
33+
'm': '', // Hackmud: #23381B | Discord: Gray
34+
'n': '', // Hackmud: #00535B | Discord: Cyan
35+
'o': '', // Hackmud: #324A4C | Discord: Cyan
36+
'p': '', // Hackmud: #0073A6 | Discord: Blue
37+
'q': '', // Hackmud: #385A6C | Discord: Blue
38+
'r': '', // Hackmud: #010067 | Discord: Blue
39+
's': '', // Hackmud: #507AA1 | Discord: Blue
40+
't': '', // Hackmud: #601C81 | Discord: Pink
41+
'u': '', // Hackmud: #43314C | Discord: Gray
42+
'v': '', // Hackmud: #8C0069 | Discord: Pink
43+
'w': '', // Hackmud: #973984 | Discord: Pink
44+
'x': '', // Hackmud: #880024 | Discord: Red
45+
'y': '', // Hackmud: #762E4A | Discord: Red
46+
'z': '', // Hackmud: #101215 | Discord: Gray
47+
'A': '', // Hackmud: #FFFFFF | Discord: White
48+
'B': '', // Hackmud: #CACACA | Discord: White
49+
'C': '', // Hackmud: #9B9B9B | Discord: Gray
50+
'D': '', // Hackmud: #FF0000 | Discord: Red
51+
'E': '', // Hackmud: #FF8383 | Discord: Red
52+
'F': '', // Hackmud: #FF8000 | Discord: Yellow
53+
'G': '', // Hackmud: #F3AA6F | Discord: Yellow
54+
'H': '', // Hackmud: #FBC803 | Discord: Yellow
55+
'I': '', // Hackmud: #FFD863 | Discord: Yellow
56+
'J': '', // Hackmud: #FFF404 | Discord: Yellow
57+
'K': '', // Hackmud: #F3F998 | Discord: Green
58+
'L': '', // Hackmud: #1EFF00 | Discord: Green
59+
'M': '', // Hackmud: #B3FF9B | Discord: Green
60+
'N': '', // Hackmud: #00FFFF | Discord: Cyan
61+
'O': '', // Hackmud: #8FE6FF | Discord: Cyan
62+
'P': '', // Hackmud: #0070DD | Discord: Blue
63+
'Q': '', // Hackmud: #A4E3FF | Discord: Blue
64+
'R': '', // Hackmud: #0000FF | Discord: Blue
65+
'S': '', // Hackmud: #7AB2F4 | Discord: Blue
66+
'T': '', // Hackmud: #B035EE | Discord: Pink
67+
'U': '', // Hackmud: #E6C4FF | Discord: Pink
68+
'V': '', // Hackmud: #FF00EC | Discord: Pink
69+
'W': '', // Hackmud: #FF96E0 | Discord: Pink
70+
'X': '', // Hackmud: #FF0070 | Discord: Red
71+
'Y': '', // Hackmud: #FF6A98 | Discord: Red
72+
'Z': '', // Hackmud: #0C112B | Discord: Gray
73+
};
74+
75+
function Formatter(message) {
76+
function convertHackmudColors(text) {
77+
// Regex to detect backtick-wrapped strings with a leading color code
78+
const regex = /`([a-zA-Z0-9])([^`]*)`/g;
79+
return text.replace(regex, (match, code, content) => {
80+
// Map the color code to the corresponding Discord color if it exists
81+
const discordColor = hackmudToDiscordColors[code];
82+
if (discordColor) {
83+
return `${discordColor}${content}${hackmudToDiscordColors.reset}`;
84+
}
85+
// If no color is found, return the original match
86+
return match;
87+
});
88+
}
89+
90+
// Extract timestamp and format it
91+
const timestamp = new Date(message.t * 1000);
92+
const hours = timestamp.getHours().toString().padStart(2, '0');
93+
const minutes = timestamp.getMinutes().toString().padStart(2, '0');
94+
const formattedTime = `${hours}${minutes}`;
95+
96+
// Format user, channel, and other elements
97+
const formattedUser = `${message.from_user}`;
98+
const formattedChnlBlue = `${message.channel}`;
99+
const formattedChnlPink = `${message.channel}`;
100+
const formattedTell = `tell`;
101+
const messageBord = `:::`;
102+
103+
let formattedMessage = "";
104+
105+
// Apply color conversion to the message text
106+
const convertedMessage = convertHackmudColors(message.msg);
107+
108+
if (message.is_join) {
109+
formattedMessage = `\`\`\`ansi\n${formattedTime} ${formattedChnlBlue} ${formattedUser} ${messageBord} ${convertedMessage} ${messageBord}\n\`\`\``;
110+
} else {
111+
if (!message.channel) {
112+
formattedMessage = `\`\`\`ansi\n${formattedTime} ${formattedTell} ${formattedUser} ${messageBord} ${convertedMessage} ${messageBord}\n\`\`\``;
113+
} else {
114+
formattedMessage = `\`\`\`ansi\n${formattedTime} ${formattedChnlPink} ${formattedUser} ${messageBord} ${convertedMessage} ${messageBord}\n\`\`\``;
115+
}
116+
}
117+
118+
return formattedMessage;
119+
}
120+
121+
function NowToRubyTS() {
122+
return Math.floor(Date.now() / 1000);
123+
}
124+
125+
function fiveMinutesAgoToRubyTS() {
126+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
127+
return Math.floor(fiveMinutesAgo / 1000);
128+
}
129+
130+
async function loadChannelMappings() {
131+
const mapRaw = await readFile(path.resolve(__dirname, '../../channelMappings.json'), 'utf8');
132+
return JSON.parse(mapRaw);
133+
}
134+
135+
async function loadPullUsers() {
136+
const configRaw = await readFile(path.resolve(__dirname, '../../config.json'), 'utf8');
137+
const config = JSON.parse(configRaw);
138+
return config.pullusers || [];
139+
}
140+
141+
async function loadMudToken() {
142+
const configRaw = await readFile(path.resolve(__dirname, '../../config.json'), 'utf8');
143+
const config = JSON.parse(configRaw);
144+
return config.mudtoken || [];
145+
}
146+
147+
module.exports = {
148+
category: 'mud',
149+
data: new SlashCommandBuilder()
150+
.setName('client')
151+
.setDescription('Commands to manage the client')
152+
.addSubcommand(subcommand =>
153+
subcommand
154+
.setName('start')
155+
.setDescription('Starts the bot')
156+
)
157+
.addSubcommand(subcommand =>
158+
subcommand
159+
.setName('stop')
160+
.setDescription('Stops the bot')
161+
),
162+
async execute(interaction) {
163+
const option = interaction.options.getSubcommand();
164+
if (option === 'start') {
165+
if (getChatPullInterval()) {
166+
await interaction.reply('Chat pull loop is already running.');
167+
return;
168+
}
169+
170+
await interaction.client.user.setStatus('online');
171+
await interaction.client.user.setActivity('Listening for new messages...', { type: 'WATCHING' });
172+
173+
await interaction.reply('Bot initialized. Listening for new messages...');
174+
175+
let lastTimestamp = fiveMinutesAgoToRubyTS();
176+
177+
async function fetchNewMessages() {
178+
const channelMappings = await loadChannelMappings();
179+
const pullusers = await loadPullUsers();
180+
const mudtoken = await loadMudToken();
181+
182+
const apiUrl = 'https://www.hackmud.com/mobile/chats.json';
183+
const payload = {
184+
chat_token: mudtoken,
185+
usernames: pullusers,
186+
after: lastTimestamp,
187+
};
188+
189+
try {
190+
const response = await fetch(apiUrl, {
191+
method: 'POST',
192+
headers: { 'Content-Type': 'application/json' },
193+
body: JSON.stringify(payload),
194+
});
195+
196+
const result = await response.json();
197+
198+
if (result.ok) {
199+
Object.entries(result.chats).forEach(([user, messages]) => {
200+
if (messages.length === 0) {
201+
//console.log(`No new messages for user: ${user}`);
202+
} else {
203+
messages.forEach(async (message) => {
204+
const discordChannelId = channelMappings[user];
205+
206+
if (discordChannelId) {
207+
const formattedMessage = Formatter(message);
208+
209+
// Fetch the Discord channel and send the message
210+
const channel = interaction.client.channels.cache.get(discordChannelId);
211+
212+
if (channel) {
213+
const result = await channel.send(formattedMessage);
214+
if (result.code === 50013) {
215+
console.log(`No permission to send message in ${channel.name}, message not sent`);
216+
return
217+
}
218+
}
219+
}
220+
});
221+
}
222+
});
223+
224+
// Update the last timestamp
225+
lastTimestamp = NowToRubyTS();
226+
} else {
227+
console.error('Hackmud API error:', result.msg || 'Unknown error');
228+
}
229+
} catch (error) {
230+
console.error('Error fetching messages:', error);
231+
}
232+
}
233+
234+
const interval = setInterval(fetchNewMessages, 5000);
235+
setChatPullInterval(interval);
236+
}
237+
if (option === 'stop') {
238+
if (getChatPullInterval()) {
239+
clearChatPullInterval();
240+
241+
await interaction.client.user.setStatus('idle');
242+
await interaction.client.user.setActivity(null);
243+
244+
await interaction.reply('Chat pull loop has been stopped.');
245+
} else {
246+
await interaction.reply('The chat pull loop is not running.');
247+
}
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)