-
Notifications
You must be signed in to change notification settings - Fork 8
/
index.js
251 lines (223 loc) · 10.3 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
const keep_alive = require('./keep_alive.js')
const config = require('./config.js')
const Discord = require('discord.js');
const client = new Discord.Client();
const token = process.env.DISCORD_BOT_SECRET;
const MEETING_CMD = '!meeting'
const EDIT_CMD = '!edit'
const APPEND_CMD = '!append'
// keys must be lowercase
const KEY_LENGTH_MINS = '--duration'.toLowerCase()
const KEY_CHANNEL = '--channel'.toLowerCase()
const KEY_OUTPUT_CHANNEL = '--outputchannel'.toLowerCase()
const REQUIRED_KEYS = [KEY_LENGTH_MINS]
const DOCUMENTATION_LINK = 'https://github.com/blueridger/MeetingAttendanceDiscordBot/blob/mainline/README.md'
function* batch(arr, n = 1) {
const l = arr.length
let i = 0;
while (i <= l) {
yield arr.slice(i, Math.min(i + n, l))
i += n
}
}
function sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
client.on('ready', () => {
console.log("I'm in");
console.log(client.user.username);
});
client.on('message', async msg => {
if (msg.author.id === client.user.id) {
return;
}
handleAdminEdit(msg);
handleAdminAppend(msg);
if (msg.content.startsWith(MEETING_CMD)) {
try {
const args = parseArgumentsFromMessage(msg)
if (!args) return
let {
metadata,
voiceChannelSubstring,
outputChannelSubstring,
durationMins
} = args
let voiceChannel;
if (voiceChannelSubstring) {
voiceChannel = msg.guild.channels.cache.find(channel => channel.type === 'voice' && channel.name.toLowerCase().includes(voiceChannelSubstring))
} else if (msg.member.voice.channel) {
voiceChannel = msg.member.voice.channel
}
let outputChannel = msg.channel
if (outputChannelSubstring) {
outputChannel = msg.guild.channels.cache.find(channel => channel.type === 'text' && channel.name.toLowerCase().includes(outputChannelSubstring))
}
if (!voiceChannel) {
msg.author.send(`Voice channel was not found. Please join a voice channel or use the \`${KEY_CHANNEL}\` parameter.`)
return
}
const meetingMsg = await outputChannel.send(metadata + '\nParticipants: (watching)')
watchChannel(
meetingMsg,
msg,
durationMins,
metadata,
voiceChannel,
)
} catch (e) {
console.log(e)
try {
msg.author.send(
"An error was thrown. Please have the bot maintainer check the console log for details."
)
} catch (e) {
console.log(e)
}
}
}
});
function parseMessageLinkParam(msg) {
const split = msg.content.slice(0, msg.content.indexOf('\n') > 0 ? msg.content.indexOf('\n') : msg.content.length).split(" ");
if (split.length !== 2) {
msg.author.send(
"Usage:\n!edit messageLink\n!append messageLink\nExample:\n!edit https://discord.com/channels/798975165342023700/798975167971196981/913128715372859412"
)
return null
}
return split[1].match(/https\:\/\/discord\.com\/channels\/[0-9]*\/([0-9]*)\/([0-9]*)/)
}
async function handleAdminEdit(msg) {
if (msg.content.startsWith(EDIT_CMD) && msg.member.roles.cache.get(config.ADMIN_ROLE)) {
const parts = parseMessageLinkParam(msg);
if (!parts) return;
const [, channelId, messageId] = parts;
const targetMsg = await client.channels.cache.get(channelId).messages.fetch(messageId);
if (targetMsg && targetMsg.author.id === client.user.id) {
const metadata = msg.content.slice(msg.content.indexOf('\n') > 0 ? msg.content.indexOf('\n') : msg.content.length);
const suffix = "\n" + targetMsg.content.slice(targetMsg.content.lastIndexOf('\n') > 0 ? targetMsg.content.lastIndexOf('\n') + 1 : 0);
await targetMsg.edit(metadata + suffix);
}
} else if (msg.content.startsWith(EDIT_CMD) && !msg.member.roles.cache.get(config.ADMIN_ROLE)) {
msg.author.send("You don't have the correct role to use that command.")
}
}
async function handleAdminAppend(msg) {
if (msg.content.startsWith(APPEND_CMD) && msg.member.roles.cache.get(config.ADMIN_ROLE)) {
const parts = parseMessageLinkParam(msg);
if (!parts) return;
const [, channelId, messageId] = parts;
const targetMsg = await client.channels.cache.get(channelId).messages.fetch(messageId);
if (targetMsg && targetMsg.author.id === client.user.id) {
const newMetadata = msg.content.slice(msg.content.indexOf('\n') > 0 ? msg.content.indexOf('\n') : msg.content.length);
const oldMetadata = parseMetadataFromMeetingMessage(targetMsg);
const participants = parseParticipantsFromMeetingMessage(targetMsg);
await targetMsg.edit(oldMetadata + newMetadata + participants);
}
} else if (msg.content.startsWith(APPEND_CMD) && !msg.member.roles.cache.get(config.ADMIN_ROLE)) {
msg.author.send("You don't have the correct role to use that command.")
}
}
function parseMetadataFromMeetingMessage(msg) {
const lastParticipantsIndex = msg.content.indexOf('\nParticipants:') > 0 ? msg.content.lastIndexOf('\nParticipants:') : 0
return msg.content.slice(0, lastParticipantsIndex)
}
function parseParticipantsFromMeetingMessage(msg) {
const lastLineBreakIndex = msg.content.indexOf('\n') > 0 ? msg.content.lastIndexOf('\n') : 0
return (lastLineBreakIndex ? "" : "\n") + msg.content.slice(lastLineBreakIndex)
}
function parseArgumentsFromMessage(msg) {
let voiceChannelSubstring, outputChannelSubstring, durationMins;
const firstLineBreakIndex = msg.content.indexOf('\n') > 0 ? msg.content.indexOf('\n') : msg.content.length
const command = msg.content.slice(0, firstLineBreakIndex).toLowerCase().match(/\S+/g) || []
const metadata = msg.content.slice(firstLineBreakIndex)
try {
if (command.includes(KEY_CHANNEL))
voiceChannelSubstring = command[command.indexOf(KEY_CHANNEL) + 1]
if (command.includes(KEY_OUTPUT_CHANNEL))
outputChannelSubstring = command[command.indexOf(KEY_OUTPUT_CHANNEL) + 1]
if (command.includes(KEY_LENGTH_MINS))
durationMins = command[command.indexOf(KEY_LENGTH_MINS) + 1]
} catch {
msg.author.send(`Failed to parse. Unexpected end of parameters.\n${DOCUMENTATION_LINK}`)
return null
}
if (REQUIRED_KEYS.filter(key => !command.includes(key)).length) {
msg.author.send(`A required key was not found. Please provide the keys: ${REQUIRED_KEYS}\n${DOCUMENTATION_LINK}`)
return null
}
if (isNaN(parseInt(durationMins, 10)) || durationMins < 1 || durationMins > 120) {
msg.author.send(`${KEY_LENGTH_MINS} [${durationMins}] was not valid. Please provide an integer between 1 and 120.`)
return null
}
durationMins = parseInt(durationMins, 10)
return { metadata, voiceChannelSubstring, outputChannelSubstring, durationMins }
}
async function watchChannel(
meetingMsg,
commandMsg,
durationMins,
metadata,
voiceChannel,
) {
let expiration = new Date(meetingMsg.createdAt)
expiration.setMinutes(expiration.getMinutes() + durationMins)
const participantMentions = new Set()
let updatedMetadata = metadata
let actualMetadata = metadata
let updatedArgs = parseArgumentsFromMessage(await commandMsg.fetch())
let totalIntervals = 0
const intervalCounts = new Map();
console.log(`[${meetingMsg.id}] Starting to watch channel [${voiceChannel.id}].`)
while (Date.now() < expiration) {
totalIntervals++
(await voiceChannel.fetch()).members.each((member) => {
if (!member.user.bot) {
const mention = member.user.toString()
participantMentions.add(mention)
intervalCounts.set(mention, (intervalCounts.get(mention) || 0) + 1)
}
})
const participantsString = `\nParticipants: (watching) ${Array.from(participantMentions).join(' ')}`
actualMetadata = parseMetadataFromMeetingMessage(await meetingMsg.fetch())
updatedArgs = updatedArgs ? parseArgumentsFromMessage(await commandMsg.fetch()) : null
if (updatedArgs) {
if (updatedMetadata != updatedArgs.metadata) {
updatedMetadata = updatedArgs.metadata;
actualMetadata = updatedMetadata;
}
expiration = new Date(meetingMsg.createdAt)
expiration.setMinutes(expiration.getMinutes() + updatedArgs.durationMins)
}
await meetingMsg.edit(actualMetadata + participantsString)
await meetingMsg.suppressEmbeds(true)
console.log(`[${meetingMsg.id}] Current list: [${Array.from(participantMentions)}].`)
await sleep(config.WAIT_INTERVAL_SECONDS)
}
for (const [key, value] of intervalCounts) {
if (value < config.DECIMAL_PERCENT_ATTENDANCE_REQUIRED * totalIntervals)
participantMentions.delete(key)
}
console.log(`[${meetingMsg.id}] List after removals: [${Array.from(participantMentions)}].`)
console.log(`[${meetingMsg.id}] Finished watching participants. Watching for edits.`)
const participantsString = `\nParticipants: ${Array.from(participantMentions).join(' ')}`
expiration.setMinutes(expiration.getMinutes() + config.WATCH_EDITS_AFTER_MEETING_MINUTES)
while (Date.now() < expiration) {
actualMetadata = parseMetadataFromMeetingMessage(await meetingMsg.fetch())
updatedArgs = updatedArgs ? parseArgumentsFromMessage(await commandMsg.fetch()) : updatedArgs
if (updatedArgs) {
if (updatedMetadata != updatedArgs.metadata) {
updatedMetadata = updatedArgs.metadata;
actualMetadata = updatedMetadata;
}
}
await meetingMsg.edit(actualMetadata + participantsString)
await meetingMsg.suppressEmbeds(true)
await sleep(config.WAIT_INTERVAL_SECONDS)
}
console.log(`[${meetingMsg.id}] Finished watching for edits.`)
await meetingMsg.edit(updatedMetadata + participantsString)
await meetingMsg.suppressEmbeds(true)
console.log(`[${meetingMsg.id}] Finished thread.`)
}
client.login(token);