-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmain.js
393 lines (350 loc) · 12 KB
/
main.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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
const cron = require('cron')
const { Client, Intents, Permissions } = require('discord.js')
const config = require('./config.json')
const SQLite = require('better-sqlite3')
const sql = new SQLite('./historicalPairs.sqlite')
const client = new Client({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_MESSAGES,
Intents.FLAGS.GUILD_MEMBERS,
],
})
let status = 'init'
let interval = 7
let matchingJob = getCronJob(interval)
let groupSize = 2
let roles = []
// Wait to load guild and roles until bot is ready
let guild
function getCronJob() {
// return cron job with interval in seconds
return new cron.CronJob(`*/${interval} * * * * *`, async () => {
// return cron job with interval in days
// return new cron.CronJob(`* * * */${interval} * *`, () => { // days
let groups = await getNewGroups()
console.log('Groups: ')
console.log(groups)
deleteMatchingChannels()
createPrivateChannels(groups)
})
}
async function getParticipatingUserIDs() {
try {
await guild.members.fetch()
let participatingUserIDs = new Set()
console.log('Roles', roles)
roles.forEach((roleName) => {
const Role = guild.roles.cache.find((role) => role.name == roleName)
usersWithRole = guild.roles.cache
.get(Role.id)
.members.map((m) => m.user.id)
usersWithRole.forEach((user) => {
participatingUserIDs.add(user)
})
})
return participatingUserIDs
} catch (err) {
console.error(err)
}
return
}
/**
* Store historical pairs in db
*
* @param {[[]]} pairs
*/
function setHistoricalPairs(pairs) {
const stmt = sql.prepare(
'INSERT INTO pairs (user1_id, user2_id) VALUES (@user1_id, @user2_id);'
)
const insertPairs = sql.transaction((pairs) => {
pairs.forEach((p) => {
values = { user1_id: p[0], user2_id: p[1] }
stmt.run(values)
})
})
const insertPairsReversed = sql.transaction((pairs) => {
pairs.forEach((p) => {
values = { user1_id: p[1], user2_id: p[0] }
stmt.run(values)
})
})
insertPairs(pairs)
insertPairsReversed(pairs)
}
/**
* Get all users' historical pairs
*
* @returns {[]} Array of all rows in the table pairs
*/
function getAllData() {
let smt = sql.prepare('SELECT * FROM pairs;')
let rows = smt.all()
return rows
}
/**
* Get all users' historical pairs
*
* @param {string[]} userIDs
* @returns {{[userID: string]: Set}} Object with data on each user's previous pairs
*/
function getHistoricalPairs(userIDs) {
const stmt = sql.prepare('SELECT * FROM pairs WHERE user1_id = ?')
let pairs = {}
userIDs.forEach((userID) => {
pairs[userID] = new Set()
let tmpPairs = stmt.all(userID)
tmpPairs.forEach((tmpPair) => {
pairs[userID].add(tmpPair['user2_id'])
})
})
return pairs
}
/**
* Helper function to delete all private channels
*/
async function deleteMatchingChannels() {
let channels = client.channels.cache.filter(
(channel) => channel.name === config.matchingChannelName
)
channels = Array.from(channels.values())
for (let i = 0; i < channels.length; i++) {
channels[i].delete()
}
}
/**
* Fischer-Yates Shuffle Algorithm
*
* @param {[Array]} array
* @returns {Array} Shuffled array
*/
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const temp = array[i]
array[i] = array[j]
array[j] = temp
}
return array
}
/**
* Constructs new groups based on historical pairs
* https://lifeat.tails.com/how-we-made-bagelbot/
*
* @returns {[string[]]} New groups of the form [[user_1_ID, user_2_ID], [user_3_ID, user_4_ID], ...]
*/
async function getNewGroups() {
let participatingUserIDs = await getParticipatingUserIDs()
console.log(participatingUserIDs)
let historicalPairs = getHistoricalPairs(participatingUserIDs)
// Can use simple data below for basic testing until getParticipatingUserIDs() is implemented
// let participatingUserIDs = ['0', '1', '2', '3', '4', '5']
// let historicalPairs = {
// 2: new Set('3'),
// 3: new Set('2'),
// }
let newGroups = []
// Convert set to an array to allow for shuffling
let unpairedUserIDs = Array.from(participatingUserIDs)
unpairedUserIDs = shuffle(unpairedUserIDs)
// Keep track of users that have been paired
const pairedUsersStatus = new Array(unpairedUserIDs.length).fill(false)
for (let i = 0; i < unpairedUserIDs.length; i++) {
if (pairedUsersStatus[i]) {
// The user has been paired already
continue
}
// This is the user for which we will try to find a pair
const userID = unpairedUserIDs[i]
// Get all other users that are unpaired
const filteredUnpairedIDs = unpairedUserIDs.filter(
(id, index) => id !== userID && !pairedUsersStatus[index]
)
// Keep track of the ID to pair the user with (either fall-back or successful pairing of users that have not met)
let newPairingID
// If there are only 2 or 3 people left, there exists only one possible pairing
if (
filteredUnpairedIDs.length === 2 ||
filteredUnpairedIDs.length === 1
) {
newGroups.push([userID, ...filteredUnpairedIDs])
break
}
// Fall-back pairing
newPairingID = filteredUnpairedIDs[0]
// User's previous pairs
const userHistoricalPairs = historicalPairs[userID]
// Attempt to pair users who have not met
for (const potentialPairingID of filteredUnpairedIDs) {
// Check to see if the users have met before
if (
userHistoricalPairs &&
userHistoricalPairs.has(potentialPairingID)
) {
continue
} else {
// The pair has not met yet so assign them together
newPairingID = potentialPairingID
break
}
}
newGroups.push([userID, newPairingID])
// Mark the users as paired
pairedUsersStatus[i] = true
pairedUsersStatus[unpairedUserIDs.indexOf(newPairingID)] = true
}
return newGroups
// client.setScore = sql.prepare("INSERT OR REPLACE INTO pairs (id, user1_id, user2_id) VALUES (@id, @user1_id, @user2_id);");
// => write to SQL lite db
}
/**
* Create private channels with the paired users
*
* @param {[string[]]} userIDGroups Array of grouped User ID's of the form [[user_1_ID, user_2_ID], [user_3_ID, user_4_ID], ...]
* @returns {void}
*/
async function createPrivateChannels(userIDGroups) {
if (!guild) return
// Iterate over userID pairings and create DM group
for (const userIDPair of userIDGroups) {
// Construct permission overwrite for each user in the pair
const userPermissionOverWrites = userIDPair.map((userID) => {
return {
type: 'member',
id: userID,
allow: Permissions.ALL,
}
})
// Create private channel
let channel = await guild.channels.create(config.matchingChannelName, {
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: Permissions.ALL,
},
// Add the overwrites for the pair of users
...userPermissionOverWrites,
],
})
console.log(channel.channels)
client.channels.cache.get(channel.id).send(`
Hello there,
You have been matched!
Schedule a call go for a walk or do whatever else.
The channel will automatically closed after ${interval} days.
`)
}
}
client.on('ready', async () => {
console.log(`Logged in as ${client.user.tag}!`)
// Check if the table "pairs" exists.
const table = sql
.prepare(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name = 'pairs';"
)
.get()
if (!table['count(*)']) {
// If the table isn't there, create it and setup the database correctly.
sql.prepare(
'CREATE TABLE pairs (id INTEGER PRIMARY KEY AUTOINCREMENT, user1_id TEXT, user2_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);'
).run()
// Ensure that the "id" row is always unique and indexed.
sql.prepare('CREATE INDEX idx_users ON pairs (user1_id);').run()
sql.pragma('synchronous = 1')
sql.pragma('journal_mode = wal')
}
// Load guild once bot is ready
guild = client.guilds.cache.get(config.guildID)
})
client.on('messageCreate', async (message) => {
// if the author is another bot OR the command is not in the bot communications channel OR the command doesn't start with the correct prefix => ignore
if (
message.author.bot ||
message.channelId !== config.botCommunicationChannelID ||
message.content.indexOf(config.prefix) !== 0
)
return
// extract command and arguments from message
const args = message.content.slice(config.prefix.length).trim().split(/ +/g)
const command = args.shift()
// log command and arg on console (for debugging)
console.log('Command: ', command)
console.log('Args: ', args)
if (command === 'alive') {
message.channel.send('Alive')
}
if (command === 'help') {
message.channel.send('Available commands:')
message.channel.send(
'/setRoles <name of role1> <name of role2> <name of role3> ... => members of which role should be included in the matching process'
)
message.channel.send(
'/setInterval <int> => how often (in days) should the matching process be triggered'
)
message.channel.send(
'/setGroupSize <int> => how many members should be included in one matching group'
)
message.channel.send('/status => get current status of the bot')
message.channel.send('/pause => pause bot')
message.channel.send('/resume or /start => resume or start bot')
message.channel.send('/alive => check if bot is still alive')
}
if (command === 'status') {
message.channel.send(`Status: ${status}`)
message.channel.send(`Roles: ${roles}`)
message.channel.send(`Interval: ${interval}`)
message.channel.send(`GroupSize: ${groupSize}`)
}
if (command === 'pause') {
status = 'paused'
message.channel.send(`New status: ${status}`)
matchingJob.stop()
}
if (command === 'resume' || command === 'start') {
status = 'active'
message.channel.send(`New status: ${status}`)
matchingJob.start()
}
if (command === 'setRoles') {
roles = args.slice(/ +/g)
message.reply(`New Roles: ${roles}`)
}
if (command === 'setInterval') {
let newInterval = args[0]
interval = newInterval
matchingJob.stop()
matchingJob = getCronJob()
matchingJob.start()
message.reply(`New interval: ${args}`)
message.reply('Matching restarted.')
}
if (command === 'setGroupSize') {
groupSize = args[0]
message.reply(`New group size: ${groupSize}`)
}
if (command === 'deleteChannels') {
deleteMatchingChannels()
}
if (command === 'testDB') {
let historicalPairs = [
['1', '2'],
['1', '3'],
['1', '4'],
['1', '5'],
]
setHistoricalPairs(historicalPairs)
let pairs = getHistoricalPairs(['2'])
let matches = pairs['2']
message.reply(`User 2 was matched with: ${Array.from(matches)}`)
}
if (command === 'testMatch') {
roles = ['Outlier']
let groups = await getNewGroups()
console.log('Groups: ')
console.log(groups)
deleteMatchingChannels()
createPrivateChannels(groups)
}
})
client.login(config.token)