Skip to content
This repository was archived by the owner on Nov 10, 2021. It is now read-only.

Queue level updates on bot startup #79

Merged
merged 4 commits into from
Mar 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions helpers/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,50 @@ const commandFiles = readdirSync('./triggers/commands').filter((file) =>
file.endsWith('.ts')
);
for (const file of commandFiles) {
console.log(`Importing command ${file}`);
const command: Command = require(`../triggers/commands/${file}`);
commands.set(command.name, command);
console.log(`Imported command ${command.name}`);
// console.log(`Importing command ${file}`);
try {
const command: Command = require(`../triggers/commands/${file}`);
commands.set(command.name, command);
console.log(`Imported command ${command.name}`);
} catch (e) {
console.warn(
`[WARN]\tCould not import command ${file.slice(0, -3)}\n`,
e
);
}
}
console.log('Imported commands\n');
// console.log('Imported commands\n');

export const triggers = new Collection<string, TriggeredCommand>();
const triggerFiles = readdirSync('./triggers/triggers').filter((file) =>
file.endsWith('.ts')
);
for (const file of triggerFiles) {
const name = file.slice(0, -3);
console.log(`Importing trigger ${file}`);
const trigger: TriggeredCommand = require(`../triggers/triggers/${file}`);
triggers.set(name, trigger);
console.log(`Imported trigger ${name}`);
// console.log(`Importing trigger ${file}`);
try {
const trigger: TriggeredCommand = require(`../triggers/triggers/${file}`);
triggers.set(name, trigger);
console.log(`Imported trigger ${name}`);
} catch (e) {
console.warn(`[WARN]\tCould not import trigger ${name}\n`, e);
}
}
console.log('Imported triggers\n');
// console.log('Imported triggers\n');

export const reactions = new Collection<string, ReactionCommand>();
const reactionFiles = readdirSync('./triggers/reactions').filter((file) =>
file.endsWith('.ts')
);
for (const file of reactionFiles) {
const name = file.slice(0, -3);
console.log(`Importing reaction command ${file}`);
const reaction: ReactionCommand = require(`../triggers/reactions/${file}`);
reactions.set(name, reaction);
console.log(`Imported reaction command ${name}`);
// console.log(`Importing reaction command ${file}`);
try {
const reaction: ReactionCommand = require(`../triggers/reactions/${file}`);
reactions.set(name, reaction);
console.log(`Imported reaction command ${name}`);
} catch (e) {
console.warn(`[WARN]\tCould not import trigger ${name}\n`, e);
}
}
console.log('Imported reaction commands\n');
// console.log('Imported reaction commands\n');
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { readyMembers, setupMemberListeners } from './helpers/members';
import { readyVC, setupVCListeners } from './helpers/vc';
import { setupMessageListeners } from './helpers/messageHandler';
import { setupReactionListeners } from './helpers/reactionHandler';
import { queueLevelUpdates } from './util/levels';

client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}`);
client.user.setPresence({ activity: { type: 'WATCHING', name: '🐐' } });

readyMembers();
readyVC();

queueLevelUpdates();
});

setupMessageListeners();
Expand Down
8 changes: 6 additions & 2 deletions triggers/commands/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { EmbedFieldData, MessageEmbed } from 'discord.js';
import { client, config } from '../..';
import { Command } from '../../Types';
import { commandAllowed } from '../../util/commands';
import { getRankedLeaderboard, redisClient } from '../../util/levels';
import {
getLevelNumber,
getRankedLeaderboard,
redisClient,
} from '../../util/levels';
import { getChannelList, getRandomHexColor } from '../../util/text';

const PAGE_SIZE = 10;
Expand Down Expand Up @@ -38,7 +42,7 @@ module.exports = <Command>{
onPage.forEach((val, index) => {
const user = client.users.cache.get(val.uid);
const xp = `${val.data.xp.toLocaleString()} Exp.`.padEnd(14);
const level = `Lvl. ${val.data.level}`.padEnd(10);
const level = `Lvl. ${getLevelNumber(val.data.xp)}`.padEnd(10);
const messages = `${val.data.count.toLocaleString()} Message${
val.data.count != 1 ? 's' : ''
}`.padEnd(14);
Expand Down
25 changes: 20 additions & 5 deletions triggers/triggers/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
redisClient,
setUserLevel,
setLevelRoles,
queuedLevelUpdates,
} from '../../util/levels';

module.exports = <TriggeredCommand>{
Expand All @@ -28,14 +29,28 @@ module.exports = <TriggeredCommand>{
}
level.last = message.createdAt;
const newLevel = getLevelNumber(level.xp);
if (newLevel > level.level) {
message.reply(
`🎉 Congrats! You just leveled up to level ${newLevel}!`
);
const levelUpdateQueued = queuedLevelUpdates.includes(
message.author.id
);
if (newLevel != level.level || levelUpdateQueued) {
if (newLevel > level.level) {
message.reply(
`🎉 Congrats! You just leveled up to level ${newLevel}!`
);
}

const member = message.member;

setLevelRoles(member, newLevel);
const changedStuff = await setLevelRoles(member, newLevel);
if (levelUpdateQueued) {
console.log(
`${member.displayName} had an update queued so their roles were updated (something happened: ${changedStuff})`
);
queuedLevelUpdates.splice(
queuedLevelUpdates.indexOf(member.id),
1
);
}
}

await setUserLevel(uid, level);
Expand Down
128 changes: 121 additions & 7 deletions util/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createClient, RedisClient } from 'redis';
import { promisify } from 'util';
import { client, config } from '..';
import { LevelData } from '../Types';
import { coerceBool, ensureSingleInstance } from './math';

export let redisClient: RedisClient;
export let get: (key: string) => Promise<string>,
Expand Down Expand Up @@ -88,8 +89,11 @@ export function getLevelNumberFromRole(role: Role) {
return undefined;
}

export function setLevelRoles(member: GuildMember, newLevel: number) {
const newRole = member.guild.roles.cache
export async function getLevelRoleChanges(
member: GuildMember,
newLevel: number
) {
const add = member.guild.roles.cache
.filter(
(role) =>
role.name.startsWith('Level') &&
Expand All @@ -101,11 +105,121 @@ export function setLevelRoles(member: GuildMember, newLevel: number) {
)
.last();

member.roles.cache.forEach((role) =>
role.name.startsWith('Level') && role.id != newRole.id
? member.roles.remove(role)
: null
const remove: Role[] = [];
for await (const snowflakeAndRole of member.roles.cache) {
const role = snowflakeAndRole[1];
if (role.name.startsWith('Level') && role.id != add.id) {
remove.push(role);
}
}

return { add: member.roles.cache.has(add?.id) ? undefined : add, remove };
}

export async function needsLevelRoleChanges(
member: GuildMember,
newLevel: number
) {
const roleChanges = await getLevelRoleChanges(member, newLevel);
const needsChange = coerceBool(
roleChanges.add || roleChanges.remove.length
);
// console.log(roleChanges, needsChange);
return needsChange;
}

/**
* Update a member's level roles
* @param member the GuildMember to update
* @param newLevel the member's level
* @returns whether any roles were removed
*/
export async function setLevelRoles(member: GuildMember, newLevel: number) {
const { add: newRole, remove: oldRoles } = await getLevelRoleChanges(
member,
newLevel
);

const removedRoles = oldRoles.length ? true : false;
for await (const role of oldRoles) {
await member.roles.remove(role);
}

// console.log({
// newLevel,
// newRole: newRole?.name,
// oldRoles: oldRoles.map((role) => role.name),
// });
if (newRole) await member.roles.add(newRole);
return coerceBool(removedRoles || newRole);
}

export let queuedLevelUpdates: Snowflake[] = [];

export async function queueLevelUpdates() {
// const loopSpeed = 250; // The minimum time between member updates
// /*
// * 4 member updates per second should
// * hopefully leave us with enough
// * rate limit wiggle room for
// * the rest of the bot to keep working
// */
// console.log(
// `Queueing level updates with ${loopSpeed} ms min between members`
// );
// client.user.setPresence({
// status: 'dnd',
// activity: { type: 'WATCHING', name: `level updates` },
// });

// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const [gid, guild] of client.guilds.cache) {
// for awaits to keep timing predictable
// and (hopefully) handle member changes mid-run
// console.log(guild.name);
// console.log(guild.members.cache.map((member) => member.displayName));

// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const [uid] of guild.members.cache) {
const member = await guild.members.fetch(uid);
const needsChange = await needsLevelRoleChanges(
member,
(await getUserLevel(member.id)).level
);

// console.log(
// member.displayName,
// `${needsChange ? 'needs change' : 'does not need change'}`
// );
if (needsChange) {
queuedLevelUpdates.push(member.id);
}
// const taskStartTime = new Date();
// const hadUpdate = await setLevelRoles(
// member,
// (await getUserLevel(member.id)).level
// );
// const taskTimeElapsed =
// new Date().getTime() - taskStartTime.getTime();
// console.log(
// `Updating roles for ${
// member.displayName
// } took ${taskTimeElapsed}ms (${
// !hadUpdate ? "didn't remove any roles" : 'removed roles'
// })`
// );
// if (taskTimeElapsed < loopSpeed) {
// await delay(loopSpeed - taskTimeElapsed);
// }
}
}

queuedLevelUpdates = ensureSingleInstance(queuedLevelUpdates);
console.log(
`Queued ${queuedLevelUpdates.length} update${
queuedLevelUpdates.length != 1 ? 's' : ''
}`
);

member.roles.add(newRole);
// client.user.setPresence({ status: 'online' });
}
12 changes: 12 additions & 0 deletions util/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ export function rand(range: number[]) {
Math.min(...range)
);
}

/** Delay async execution. Returns a promise that resolves after a specified time */
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export function coerceBool(value: unknown) {
return value ? true : false;
}

export const ensureSingleInstance = <T>(array: T[]) =>
array.filter((value, index) => array.indexOf(value) == index);