-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
315 lines (292 loc) · 13.9 KB
/
index.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
'use strict';
if (process.version.slice(1).split(".")[0] < 16)
throw new Error("Node 16.6.0 or higher is required.");
import { Message } from 'discord.js';
import admin from 'firebase-admin';
import klaw from 'klaw';
import path from 'path';
import * as Firebase from './api';
import { EmbedBase, CommunityPoll, ReactionCollector, SentenceService, CloudConfig, CommunityClaimEvent } from './classes';
import bot from './bot';
//formally, dotenv shouldn't be used in prod, but because staging and prod share a VM, it's an option I elected to go with for convenience
import { config as dotenv_config } from 'dotenv';
import { scheduleJob } from 'node-schedule';
dotenv_config();
// Modify Discord.js classes to include custom methods
/**
* Await a single component interaction from the target user. All other users are sent an epheremal rejecting their attempt
* @param {Object} args Destructured arguments
* @param {User} args.user Specific `User` to await an interaction from
* @returns {Promise<MessageComponentInteraction>}
*/
Message.prototype.awaitInteractionFromUser = function ({user, ...options}) {
return this.awaitMessageComponent({
...options,
filter: (i) => {
const from_user = i.user.id === user.id;
!from_user && i.reply({
ephemeral: true,
content: `This interaction isn't meant for you!`,
});
return from_user;
},
});
};
/**
* Fetch the reactions of a Discord message, updating the `ReactionManager`'s cache in-place
* @returns {Promise<Message>} Resolves to itself once the cache update is complete
*/
Message.prototype.fetchReactions = async function () {
Object.assign(this, { ...this, ...await this.fetch() });
//deep fetch - fetch the msg, then each reaction, then each reaction's users
for (const reaction of (await this.fetch()).reactions.cache.values())
await this.reactions.resolve(reaction).users.fetch();
return this;
};
/**
* Disable all the components in a message by editing it
* @returns {Promise<Message>} Resolves to the edited message with disabled components
*/
Message.prototype.disableComponents = function () {
for(const row of this.components)
for(const comp of row.components)
comp.setDisabled();
return this.edit({components: this.components});
};
// Initialization process
const init = async function () {
//initialize firebase
admin.initializeApp({});
if(admin.apps.length === 0) bot.logger.error('Error initializing firebase app');
else bot.logger.log('Firebase succesfully initialized');
await CloudConfig.init(); //import cloud configuration settings
bot.logger.log('CloudConfig initialized');
//import commands
for await (const item of klaw('./commands')) {
const cmdFile = path.parse(item.path);
if (!cmdFile.ext || cmdFile.ext !== '.js') continue;
const cmdName = cmdFile.name.split('.')[0];
try {
const cmd = new (await import('./' + path.relative(process.cwd(), `${cmdFile.dir}${path.sep}${cmdFile.name}${cmdFile.ext}`))).default();
process.env.NODE_ENV === 'development'
? bot.commands.set(cmdName, cmd)
: cmd.category !== 'development' &&
bot.commands.set(cmdName, cmd);
//delete require.cache[require.resolve(`${cmdFile.dir}${path.sep}${cmdFile.name}${cmdFile.ext}`)];
} catch(error) {
bot.logger.error(`Error loading command file ${cmdFile.name}: ${error}`);
}
}
bot.logger.log(`Loaded ${bot.commands.size} command files`);
//import discord events
for await (const item of klaw('./events/discord')) {
const eventFile = path.parse(item.path);
if (!eventFile.ext || eventFile.ext !== '.js') continue;
const eventName = eventFile.name.split('.')[0];
try {
const event = new (await import('./' + path.relative(process.cwd(), `${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`))).default();
bot.events.set(eventName, event);
bot.on(event.event_type, (...args) => event.run(...args));
//delete require.cache[require.resolve(`${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`)];
} catch(error) {
bot.logger.error(`Error loading Discord event ${eventFile.name}: ${error}`);
}
}
bot.logger.log(`Loaded ${bot.events.size} Discord events`);
//import firebase events
for await (const item of klaw('./events/firebase')){
const eventFile = path.parse(item.path);
if (!eventFile.ext || eventFile.ext !== '.js') continue;
try {
const firebase_event = new (await import('./' + path.relative(process.cwd(), `${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`))).default();
admin.firestore().collection(firebase_event.collection).onSnapshot((snapshot) => {
if(!bot.readyAt) return; //ensure bot is initialized before event is fired
if(snapshot.empty) return;
for(const docChange of snapshot.docChanges()) {
//if doc was created before bot came online, ignore it
if(docChange.doc.createTime.toDate() < bot.readyAt) continue;
switch(docChange.type) {
case 'added':
firebase_event.onAdd(docChange.doc);
break;
case 'modified':
firebase_event.onModify(docChange.doc);
break;
case 'removed':
firebase_event.onRemove(docChange.doc);
break;
}
}
}, (err) => bot.logger.error(`FirebaseEvent error with ${firebase_event.name}: ${err}`));
bot.firebase_events.set(firebase_event.name, firebase_event);
//delete require.cache[require.resolve(`${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`)];
} catch(error) {
bot.logger.error(`Error loading Firebase event ${eventFile.name}: ${error}`);
}
}
bot.logger.log(`Loaded ${bot.firebase_events.size} Firebase events`);
//import cron events
let cron_total = 0;
for await (const item of klaw('./events/cron')) {
const eventFile = path.parse(item.path);
if (!eventFile.ext || eventFile.ext !== '.js') continue;
try {
const event = new (await import('./' + path.relative(process.cwd(), `${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`))).default();
scheduleJob(event.schedule, () => event.run());
cron_total++;
//delete require.cache[require.resolve(`${eventFile.dir}${path.sep}${eventFile.name}${eventFile.ext}`)];
} catch(error) {
bot.logger.error(`Error loading cron event ${eventFile.name}: ${error}`);
}
}
bot.logger.log(`Loaded ${cron_total} cron events`);
bot.logger.log('Connecting to Discord...');
bot.login(process.env.BOT_TOKEN).then(() => {
bot.logger.debug(`Bot succesfully initialized. Environment: ${process.env.NODE_ENV}. Version: ${bot.CURRENT_VERSION}`);
process.env.NODE_ENV !== 'development' && //send message in log channel when staging/prod bot is online
bot.logDiscord({embed: new EmbedBase({
description: `\`${process.env.NODE_ENV}\` environment online, running version ${bot.CURRENT_VERSION}`,
}).Success()});
bot.logger.log('Beginning post-initializtion sequence...');
postInit();
});
};
// post-initialization, when bot is logged in and Discord API is accessible
const postInit = async function () {
//register commands with Discord
await (async function registerCommands() {
const cmds = await bot.leyline_guild.commands.set(bot.commands.map(({ run, ...data }) => data))
.catch(err => bot.logger.error(`registerCommands err: ${err}`));
//turn each Command into an ApplicationCommand
cmds.forEach(cmd => bot.commands.get(cmd.name.replaceAll(' ', '')).setApplicationCommand(cmd));
//Register command permissions
await bot.leyline_guild.commands.permissions.set({
fullPermissions: bot.commands
.filter(c => Object.keys(bot.config.command_perms.categories).includes(c.category))
.map(({id, name, category}) => ({
id,
permissions: [
...bot.config.command_perms.categories[category],
...bot.config.command_perms?.names?.[name] || [],
],
})),
}).catch(err => bot.logger.error(`registerCommands err: ${err}`));
bot.logger.log(`Registered ${cmds.size} out of ${bot.commands.size} commands to Discord`);
})();
//import ReactionCollectors (this can be modified later to take a more generic approach)
await (async function importReactionCollectors() {
let succesfully_imported = 0;
const collectors = await admin
.firestore()
.collection(`discord/bot/reaction_collectors/`)
.where('expires', '>', Date.now())
.get();
for (const doc of collectors.docs) {
try {
const ch = await bot.channels.fetch(doc.data().channel, true, true);
const msg = await ch.messages.fetch(doc.id, true, true);
const collector = await new ReactionCollector({
type: ReactionCollector.Collectors[doc.data().type],
msg,
}).loadMessageCache(doc);
doc.data().approved ?
collector.setupApprovedCollector({duration:doc.data().expires - Date.now()}) :
collector.setupModReactionCollector({from_firestore: true, duration:doc.data().expires - Date.now()});
succesfully_imported++;
} catch (err) {
bot.logger.error(`importReactionCollectors error with doc id ${doc.id}: ${err}`);
}
}
bot.logger.log(`Imported ${succesfully_imported} ReactionCollectors from Firestore`);
return;
})();
//import active polls
await (async function importPolls() {
let succesfully_imported = 0;
const polls = await admin
.firestore()
.collection(`discord/bot/polls/`)
.where('expires', '>', Date.now())
.get();
for (const doc of polls.docs) {
try {
const ch = await bot.channels.fetch(bot.config.channels.polls, true, true);
const msg = await ch.messages.fetch(doc.id, true, true);
const embed = msg.embeds[0];
if(!embed) throw new Error('No embeds found on the fetched message');
await new CommunityPoll({
embed,
author: await bot.users.fetch(doc.data().created_by),
question: embed.title,
duration: doc.data().expires - Date.now(),
choices: doc.data().choices,
}).createCollector(msg).importFirestoreData(doc);
succesfully_imported++;
} catch (err) {
bot.logger.error(`importPolls error with doc id ${doc.id}: ${err}`);
}
}
bot.logger.log(`Imported ${succesfully_imported} polls from Firestore`);
return;
})();
//import sentences
await (async function importSentences() {
let succesfully_imported = 0;
const sentences = await admin
.firestore()
.collection(SentenceService.COLLECTION_PATH)
.where('expires', '>', Date.now())
.get();
for (const doc of sentences.docs) {
try {
SentenceService.scheduleRemoval({
bot,
id: doc.id,
data: { ...doc.data() },
});
succesfully_imported++;
} catch (err) {
bot.logger.error(`importSentences error with doc id ${doc.id}: ${err}`);
}
}
bot.logger.log(`Imported ${succesfully_imported} sentences from Firestore`);
return;
})();
//import active CommunityClaimEvents
await (async function importCommunityClaimEvents() {
let succesfully_imported = 0;
const events = await admin
.firestore()
.collection(`discord/bot/community_events/`)
.where('expires', '>', Date.now())
.get();
for (const doc of events.docs) {
try {
const ch = await bot.channels.fetch(doc.data().channel, true, true);
const msg = await ch.messages.fetch(doc.id, true, true);
const embed = msg.embeds[0];
if(!embed) throw new Error('No embeds found on the fetched message');
await new CommunityClaimEvent({
embed,
author: await bot.users.fetch(doc.data().created_by),
title: embed.title,
description: embed.descripition,
duration: doc.data().expires - Date.now(),
nft: await Firebase.getNFT(doc.data().nft),
}).createCollector(msg).importFirestoreData(doc);
succesfully_imported++;
} catch (err) {
bot.logger.error(`importCommunityClaimEvents error with doc id ${doc.id}: ${err}`);
}
}
bot.logger.log(`Imported ${succesfully_imported} CommunityClaimEvents from Firestore`);
return;
})();
bot.logger.debug('Post-initialization complete');
};
init();
// Prevent the bot from crashing on unhandled rejections
process.on("unhandledRejection", function (err, promise) {
bot.logger.error(`Unhandled rejection: ${err.name}`);
console.error("Unhandled rejection:\n", promise, "\n\nReason:\n", err);
});