-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.js
363 lines (315 loc) · 10.3 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
const TOKEN_PLACEHOLDER = 'TOKEN_HERE';
const STORE_PATH = 'data.json';
const MESSAGE_LOG_PATH = 'messages.log';
const SCRIPT_PATH = './scripts/';
const Discord = require('discord.js');
const client = new Discord.Client();
const messageLogger = require('logger').createLogger(MESSAGE_LOG_PATH);
messageLogger.format = (level, date, sMsg) => `${date.toString()};${sMsg}`;
const Store = require('data-store');
const Nltk = require('nltk');
const Nltk_ngram = new Nltk.ngram(2);
const CMD_PREFIX = '$';
var store;
var aDefaultCommands;
const remDot = sCommand => sCommand.replace(/\./g, '\\.');
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', oMsg => {
messageLog(oMsg);
if (oMsg.author.id === client.user.id) {
return;
}
// Check if command is prefixed
if (!oMsg.content.startsWith(CMD_PREFIX)) {
return;
}
try {
oMsg.content = oMsg.content.substring(CMD_PREFIX.length); // Remove prefix
let oCommand = getCommandOf(oMsg.content);
if (!oCommand) {
return;
}
// Get args from message
let tokenizedInput = oMsg.content.substring(oCommand.command.name.length).trim();
tokenizedInput = (tokenizedInput === '') ? [] : tokenizedInput.split(' '); // forces empty array if no tokens after command name
// Default commands
switch(oCommand.command.name) {
case 'add':
// Adds a custom command
if (tokenizedInput.length > 1) {
oMsg.reply(addCommand(tokenizedInput[0], tokenizedInput.slice(1).join(' ')));
} else {
oMsg.reply('Improper usage\nExpected usage: $add <command> <reply...>');
}
break;
case 'set':
// Sets a custom command, overwriting an existing if need be
if (tokenizedInput.length > 1) {
oMsg.reply(setCommand(tokenizedInput[0], tokenizedInput.slice(1).join(' ')));
} else {
oMsg.reply('Improper usage\nExpected usage: $set <command> <reply...>');
}
break;
case 'del':
// Deletes a custom command
if (tokenizedInput.length === 1) {
oMsg.reply(delCommand(tokenizedInput[0]));
} else {
oMsg.reply('Improper usage\nExpected usage: $del <command>');
}
break;
case 'help':
// Displays help page listing the commands
if (tokenizedInput.length === 1) {
// Specific command help listing for custom commands
let cmdStrPath = `commands.${remDot(tokenizedInput[0])}`;
if (store.has(cmdStrPath) && store.has(`${cmdStrPath}.help_msg`)) {
let help_msg = store.get(`${cmdStrPath}.help_msg`);
oMsg.reply(help_msg);
} else {
oMsg.reply(`No help message for command '${tokenizedInput[0]}'`);
}
} else {
// Default help listing of all commands
let sDefCommands = aDefaultCommands.join(', ');
let sCustCommands = '';
if (store.has('commands')) {
sCustCommands = Object.keys(store.get('commands')).join(', ');
}
oMsg.reply(`\n**Normal** ${sDefCommands}\n**Custom**: ${sCustCommands}`);
}
break;
case 'purge':
// Removes a specified amount of messages from the channel
if (tokenizedInput.length > 1) {
purgeCommand(oMsg, tokenizedInput[0]);
} else {
oMsg.reply('Improper usage\nExpected usage: $purge <numOfMessagesToRemove>');
}
break;
case 'restart':
// Restarts the bot
oMsg.reply('Restarting bot...');
restart();
break;
case 'sethelp':
// Sets help message for custom commands
if (tokenizedInput.length > 1) {
oMsg.reply(setHelpCommand(tokenizedInput[0], tokenizedInput.slice(1).join(' ')));
} else {
oMsg.reply('Improper usage\nExpected usage: $sethelp <command> <help_msg...>')
}
default:
// Assume it is a custom command
let sResp = oCommand.command.response;
let sScript = oCommand.command.script;
if (sResp) {
oMsg.reply(oCommand.command.response).then(() => {
if (sScript) {
scriptCommand(oMsg, sScript);
}
});
} else if (sScript) {
scriptCommand(oMsg, sScript);
}
}
} catch (oEx) {
console.error('Exception thrown! See error below');
console.trace(oEx);
}
});
function messageLog(oMsg) {
let sMsgContentEscaped = JSON.stringify(String(oMsg.content));
sMsgContentEscaped = sMsgContentEscaped.substring(1, sMsgContentEscaped.length - 1);
messageLogger.info(`${oMsg.channel}; ${oMsg.channel.name}; ${oMsg.author.id}; ${oMsg.author.username}; ${sMsgContentEscaped}`);
}
function hasToken(store) {
return store.has('token') && store.get('token') && store.get('token') !== TOKEN_PLACEHOLDER;
}
function initStore() {
let store = new Store({
path: STORE_PATH,
});
if (!hasToken(store)) {
console.log('No bot token provided, enter token in data.json and try again...');
store.set('token', TOKEN_PLACEHOLDER);
}
if (!store.has('commands')) {
store.set('commands', []);
}
if (!store.has('enabled_defaults')) {
store.set('enabled_defaults', ['add', 'del', 'set', 'help', 'purge', 'restart', 'sethelp']);
}
return store;
}
function restart() {
client.destroy()
.then(() => main());
}
function addCommand(sName, sResp) {
let sReply;
if (getCommandOf(sName)) {
sReply = 'Command already exists!';
} else {
setCommand(sName, sResp);
sReply = `Added \'${sName}\' as a command`;
}
return sReply;
}
function setCommand(sName, sResp) {
let sReply;
let oCommand = getCommandOf(sName);
if (oCommand && oCommand.command.script) {
sReply = 'Cannot overwrite commands that contain scripts';
} else {
store.set(`commands.${remDot(sName)}`, {
name: sName,
response: sResp,
});
sReply = `Set \'${sName}\' command.`;
}
return sReply;
}
function delCommand(sCommand) {
let matchedCommand = getCommandOf(sCommand);
let sReply;
if (!matchedCommand) {
sReply = 'Command doesn\'t exist';
} else if (matchedCommand.isDefault) {
sReply = 'Cannot delete default commands';
} else if (matchedCommand.command.script) {
sReply = 'Cannot delete commands with scripts';
} else {
store.del(`commands.${remDot(matchedCommand.command.name)}`);
sReply = `Deleted \'${matchedCommand.command.name}\' successfully`;
}
return sReply;
}
function purgeCommand(oMsg, sAmount) {
let iAmt = parseInt(sAmount);
if (iAmt) {
oMsg.channel.bulkDelete(iAmt)
.then(oMessages => oMsg.reply(`Bulk deleted ${oMessages.size} messages`))
.then(oReplyMsg => delay(30000, oReplyMsg))
.then(oReplyMsg => oReplyMsg.delete(1))
.catch(console.error);
} else {
oMsg.reply('Error purging messages, make sure a proper number is entered');
}
}
function setHelpCommand(sName, sHelpMsg) {
let sReply;
let matchedCommand = getCommandOf(sName);
if (!matchedCommand) {
sReply = 'Command doesn\'t exist';
} else if (matchedCommand.isDefault) {
sReply = 'Cannot set help message for default commands';
} else if (matchedCommand.command.script) {
sReply = 'Cannot set help message for commands with scripts';
} else {
store.set(`commands.${remDot(sName)}.help_msg`, sHelpMsg);
sReply = `Set help message for \'${sName}\' command.`;
}
return sReply;
}
function scriptCommand(oMsg, sScript) {
const sScriptPath = SCRIPT_PATH + sScript;
let sErrorResp;
try {
let script = require(sScriptPath).main;
if (script) {
if (script instanceof Function) {
script(client, oMsg, this).catch((ex) => {
sErrorResp = `Error: Script \'${sScript}\' failed with exception: ${ex}`;
});
} else {
sErrorResp = `Error: Script \'${sScript}\' is not a function`;
}
} else {
sErrorResp = `Error: Script \'${sScript}\' doesn\'t exist`;
}
} catch (oEx) {
sErrorResp = oEx;
} finally {
if (sErrorResp) {
oMsg.reply(`There was a problem running the script, see console for details.`);
console.error(sErrorResp);
}
}
}
function delay (t, v) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, v), t);
});
}
// Returns the closest (via Levenstein's distance) string match from the array of
// strings (aCompare). Returns null if no close matches
function getClosestString(str, aCompare) {
let closestDist = 0;
let closestString = null;
let closestMatchCount = 0; // Number of closest matches that are equally close
const minThreshold = 0.7; // minimum % match to be considered a similar string
// Find closest String
aCompare.find(sCompare => {
let dist = Nltk_ngram.sim(str, sCompare);
if (dist >= minThreshold) {
if (dist == closestDist) {
closestMatchCount ++;
}
if (dist > closestDist) {
closestDist = dist;
closestString = sCompare;
closestMatchCount = 1;
}
}
});
// If there are multiple equally close matches, then there is no clear closest string
if (closestMatchCount > 1) {
closestString = null;
}
return closestString;
}
// A wrapper function for getClosestString to allow command
// objects to be compared. The closest match will return the
// closest matched command object. Returns null if no close matches
function getClosestCommand(sCommandName, aCompareCommands) {
let closestKey = getClosestString(sCommandName, Object.keys(aCompareCommands));
return (closestKey) ? aCompareCommands[closestKey] : null;
}
function getCommandOf(sMsg) {
let sMsgCommand = sMsg.split(' ')[0];
let sMatchedCommand = getClosestString(sMsgCommand, aDefaultCommands);
let oRet;
// If matched right away, it is a default command
if (sMatchedCommand) {
oRet = {
command: {
name: sMatchedCommand,
},
isDefault: true,
};
} else {
// Custom command
if (!store.has('commands')) {
return;
}
let oMatchedCommand = getClosestCommand(sMsgCommand, store.get('commands'));
if (oMatchedCommand) {
oRet = {
command: oMatchedCommand,
isDefault: false,
};
}
}
return oRet;
}
function main() {
store = initStore();
aDefaultCommands = store.get('enabled_defaults');
if (hasToken(store)) {
client.login(store.get('token'));
}
}
main();