Updated library functions

This commit is contained in:
WatDuhHekBro 2021-04-11 03:02:56 -05:00
parent 3798c27df9
commit c980a182f8
14 changed files with 479 additions and 542 deletions

View File

@ -75,27 +75,24 @@ Because versions are assigned to batches of changes rather than single changes (
```ts
const pages = ["one", "two", "three"];
paginate(channel, author.id, pages.length, (page) => {
return {
content: pages[page]
};
});
paginate(send, page => {
return {content: pages[page]};
}, pages.length, author.id);
```
`prompt()`
`confirm()`
```ts
const msg = await channel.send('Are you sure you want to delete this?');
prompt(msg, author.id, () => {
//...
});
const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null
```
`callMemberByUsername()`
`askMultipleChoice()`
```ts
callMemberByUsername(message, args.join(" "), (member) => {
channel.send(`Your nickname is ${member.nickname}.`);
});
const result = await askMultipleChoice(await send("Which of the following numbers is your favorite?"), author.id, 4, 10000); // number (0 to 3) | null
```
`askForReply()`
```ts
const reply = await askForReply(await send("What is your favorite thing to do?"), author.id, 10000); // Message | null
```
## [src/lib](../src/lib.ts) - General utility functions

View File

@ -4,7 +4,6 @@ import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modu
import {BuyCommand, ShopCommand} from "./modules/eco-shop";
import {MondayCommand, AwardCommand} from "./modules/eco-extras";
import {BetCommand} from "./modules/eco-bet";
import {GuildMember} from "discord.js";
export default new NamedCommand({
description: "Economy command for Monika.",
@ -38,7 +37,7 @@ export default new NamedCommand({
async run({send, guild, channel, args, message, combined}) {
if (isAuthorized(guild, channel)) {
const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) send(getMoneyEmbed(member.user));
if (typeof member !== "string") send(getMoneyEmbed(member.user));
else send(member);
}
}

View File

@ -1,7 +1,7 @@
import {Command, NamedCommand, askYesOrNo} from "../../../core";
import {Command, NamedCommand, confirm} from "../../../core";
import {pluralise} from "../../../lib";
import {Storage} from "../../../structures";
import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils";
import {isAuthorized, getMoneyEmbed} from "./eco-utils";
import {User} from "discord.js";
export const BetCommand = new NamedCommand({
@ -79,88 +79,89 @@ export const BetCommand = new NamedCommand({
return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`);
// Ask target whether or not they want to take the bet.
const takeBet = await askYesOrNo(
const takeBet = await confirm(
await send(
`<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}`
),
target.id
);
if (takeBet) {
// [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
// Remove amount money from both parts at the start to avoid duplication of money.
sender.money -= amount;
receiver.money -= amount;
// Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
sender.ecoBetInsurance += amount;
receiver.ecoBetInsurance += amount;
Storage.save();
if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.id}>`);
// Notify both users.
await send(
`<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise(
amount,
"Mon",
"s"
)} has been deducted from each of them.`
// [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
// Remove amount money from both parts at the start to avoid duplication of money.
sender.money -= amount;
receiver.money -= amount;
// Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
sender.ecoBetInsurance += amount;
receiver.ecoBetInsurance += amount;
Storage.save();
// Notify both users.
send(
`<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise(
amount,
"Mon",
"s"
)} has been deducted from each of them.`
);
// Wait for the duration of the bet.
return client.setTimeout(async () => {
// In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
const sender = Storage.getUser(author.id);
const receiver = Storage.getUser(target.id);
// [TODO: when D.JSv13 comes out, inline reply to clean up.]
// When bet is over, give a vote to ask people their thoughts.
const voteMsg = await send(
`VOTE: do you think that <@${
target.id
}> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
message.id
}`
);
await voteMsg.react("✅");
await voteMsg.react("❌");
// Wait for the duration of the bet.
return client.setTimeout(async () => {
// In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
const sender = Storage.getUser(author.id);
const receiver = Storage.getUser(target.id);
// [TODO: when D.JSv13 comes out, inline reply to clean up.]
// When bet is over, give a vote to ask people their thoughts.
const voteMsg = await send(
`VOTE: do you think that <@${
target.id
}> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
message.id
}`
);
await voteMsg.react("✅");
await voteMsg.react("❌");
// Filter reactions to only collect the pertinent ones.
voteMsg
.awaitReactions(
(reaction, user) => {
return ["✅", "❌"].includes(reaction.emoji.name);
},
// [Pertinence to make configurable on the fly.]
{time: parseDuration("2m")}
)
.then((reactions) => {
// Count votes
const okReaction = reactions.get("✅");
const noReaction = reactions.get("❌");
const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0;
const no = noReaction ? (noReaction.count ?? 1) - 1 : 0;
// Filter reactions to only collect the pertinent ones.
voteMsg
.awaitReactions(
(reaction, user) => {
return ["✅", "❌"].includes(reaction.emoji.name);
},
// [Pertinence to make configurable on the fly.]
{time: parseDuration("2m")}
)
.then((reactions) => {
// Count votes
const okReaction = reactions.get("✅");
const noReaction = reactions.get("❌");
const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0;
const no = noReaction ? (noReaction.count ?? 1) - 1 : 0;
if (ok > no) {
receiver.money += amount * 2;
send(
`By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.`
);
} else if (ok < no) {
sender.money += amount * 2;
send(
`By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.`
);
} else {
sender.money += amount;
receiver.money += amount;
send(
`By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.`
);
}
if (ok > no) {
receiver.money += amount * 2;
send(
`By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.`
);
} else if (ok < no) {
sender.money += amount * 2;
send(
`By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.`
);
} else {
sender.money += amount;
receiver.money += amount;
send(
`By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.`
);
}
sender.ecoBetInsurance -= amount;
receiver.ecoBetInsurance -= amount;
Storage.save();
});
}, duration);
} else return await send(`<@${target.id}> has rejected your bet, <@${author.id}>`);
sender.ecoBetInsurance -= amount;
receiver.ecoBetInsurance -= amount;
Storage.save();
});
}, duration);
} else return;
}
})

View File

@ -1,5 +1,4 @@
import {GuildMember} from "discord.js";
import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core";
import {Command, getMemberByName, NamedCommand, confirm, RestCommand} from "../../../core";
import {pluralise} from "../../../lib";
import {Storage} from "../../../structures";
import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils";
@ -90,7 +89,7 @@ export const LeaderboardCommand = new NamedCommand({
const user = await client.users.fetch(id);
fields.push({
name: `#${i + 1}. ${user.username}#${user.discriminator}`,
name: `#${i + 1}. ${user.tag}`,
value: pluralise(users[id].money, "Mon", "s")
});
}
@ -158,42 +157,38 @@ export const PayCommand = new NamedCommand({
return send("You have to use this in a server if you want to send Mons with a username!");
const member = await getMemberByName(guild, combined);
if (!(member instanceof GuildMember)) return send(member);
if (typeof member === "string") return send(member);
else if (member.user.id === author.id) return send("You can't send Mons to yourself!");
else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!");
const target = member.user;
return prompt(
await send(
`Are you sure you want to send ${pluralise(
amount,
"Mon",
"s"
)} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`,
{
embed: {
color: ECO_EMBED_COLOR,
author: {
name: `${target.username}#${target.discriminator}`,
icon_url: target.displayAvatarURL({
format: "png",
dynamic: true
})
}
const result = await confirm(
await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, {
embed: {
color: ECO_EMBED_COLOR,
author: {
name: target.tag,
icon_url: target.displayAvatarURL({
format: "png",
dynamic: true
})
}
}
),
author.id,
() => {
const receiver = Storage.getUser(target.id);
sender.money -= amount;
receiver.money += amount;
Storage.save();
send(getSendEmbed(author, target, amount));
}
}),
author.id
);
if (result) {
const receiver = Storage.getUser(target.id);
sender.money -= amount;
receiver.money += amount;
Storage.save();
send(getSendEmbed(author, target, amount));
}
}
return;
}
})
});

View File

@ -34,12 +34,17 @@ export const ShopCommand = new NamedCommand({
const shopPages = split(ShopItems, 5);
const pageAmount = shopPages.length;
paginate(send, author.id, pageAmount, (page, hasMultiplePages) => {
return getShopEmbed(
shopPages[page],
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
);
});
paginate(
send,
(page, hasMultiplePages) => {
return getShopEmbed(
shopPages[page],
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
);
},
pageAmount,
author.id
);
}
}
});

View File

@ -42,11 +42,11 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje
description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`,
fields: [
{
name: `Sender: ${sender.username}#${sender.discriminator}`,
name: `Sender: ${sender.tag}`,
value: pluralise(Storage.getUser(sender.id).money, "Mon", "s")
},
{
name: `Receiver: ${receiver.username}#${receiver.discriminator}`,
name: `Receiver: ${receiver.tag}`,
value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s")
}
],

View File

@ -1,4 +1,4 @@
import {User, GuildMember} from "discord.js";
import {User} from "discord.js";
import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core";
// Quotes must be used here or the numbers will change
@ -70,7 +70,7 @@ export default new NamedCommand({
async run({send, message, channel, guild, author, client, args, combined}) {
const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) {
if (typeof member !== "string") {
if (member.id in registry) {
send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`);
} else {

View File

@ -20,16 +20,21 @@ export default new NamedCommand({
const commands = await getCommandList();
const categoryArray = commands.keyArray();
paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => {
const category = categoryArray[page];
const commandList = commands.get(category)!;
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`;
for (const command of commandList) output += `\n \`${command.name}\`: ${command.description}`;
return new MessageEmbed()
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category)
.setDescription(output)
.setColor(EMBED_COLOR);
});
paginate(
send,
(page, hasMultiplePages) => {
const category = categoryArray[page];
const commandList = commands.get(category)!;
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`;
for (const command of commandList) output += `\n \`${command.name}\`: ${command.description}`;
return new MessageEmbed()
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category)
.setDescription(output)
.setColor(EMBED_COLOR);
},
categoryArray.length,
author.id
);
},
any: new Command({
async run({send, message, channel, guild, author, member, client, args}) {

View File

@ -36,7 +36,7 @@ export default new NamedCommand({
async run({send, message, channel, guild, author, client, args, combined}) {
const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) {
if (typeof member !== "string") {
send(
member.user.displayAvatarURL({
dynamic: true,
@ -110,7 +110,7 @@ export default new NamedCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) {
const targetGuild = getGuildByName(combined);
if (targetGuild instanceof Guild) {
if (typeof targetGuild !== "string") {
send(await getGuildInfo(targetGuild, guild));
} else {
send(targetGuild);

View File

@ -90,17 +90,22 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author
// Gather the first page (if it even exists, which it might not if there no valid emotes appear)
if (pages > 0) {
paginate(send, author.id, pages, (page, hasMultiplePages) => {
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
paginate(
send,
(page, hasMultiplePages) => {
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
let desc = "";
for (const emote of sections[page]) {
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
}
embed.setDescription(desc);
let desc = "";
for (const emote of sections[page]) {
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
}
embed.setDescription(desc);
return embed;
});
return embed;
},
pages,
author.id
);
} else {
send("No valid emotes found by that query.");
}

View File

@ -1,15 +1,6 @@
import {
Command,
NamedCommand,
ask,
askYesOrNo,
askMultipleChoice,
prompt,
getMemberByName,
RestCommand
} from "../../core";
import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core";
import {Storage} from "../../structures";
import {User, GuildMember} from "discord.js";
import {User} from "discord.js";
import moment from "moment";
const DATE_FORMAT = "D MMMM YYYY";
@ -178,183 +169,184 @@ function getTimeEmbed(user: User) {
export default new NamedCommand({
description: "Show others what time it is for you.",
aliases: ["tz"],
async run({send, channel, author}) {
async run({send, author}) {
send(getTimeEmbed(author));
},
subcommands: {
// Welcome to callback hell. We hope you enjoy your stay here!
setup: new NamedCommand({
description: "Registers your timezone information for the bot.",
async run({send, author, channel}) {
async run({send, author}) {
const profile = Storage.getUser(author.id);
profile.timezone = null;
profile.daylightSavingsRegion = null;
let hour: number;
ask(
// Parse and validate reply
const reply = await askForReply(
await send(
"What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*"
),
author.id,
(reply) => {
hour = parseInt(reply);
if (isNaN(hour)) {
return false;
}
return hour >= 0 && hour <= 23;
},
async () => {
// You need to also take into account whether or not it's the same day in UTC or not.
// The problem this setup avoids is messing up timezones by 24 hours.
// For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
// That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days.
// (day * 24 + hour) - (day * 24 + hour)
// Since the timezones will be restricted to -12 to +14, you'll be given three options.
// The end of the month should be calculated automatically, you should have enough information at that point.
// But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day.
// 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d)
// 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d)
// 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d)
// 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d)
// For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option.
// - 23:xx same day = +0, 23:xx diff day = -1
// - 00:xx same day = +0, 00:xx diff day = +1
// - 01:xx same day = +0, 01:xx diff day = +1
// First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this:
// [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]]
// Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input.
// Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely.
// In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for.
// Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem.
// Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24
// UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12
// UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38
// Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months.
// And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms.
// That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums.
const date = new Date(); // e.g. 2021-05-01 @ 05:00
const day = date.getUTCDate(); // e.g. 1
const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29
const timezoneTupleList: [number, number, number][] = [];
const uniques: number[] = []; // only for temporary use
const duplicates = [];
// Setup the tuple list in a separate block.
for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) {
const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43)
const hour = hourSum % 24; // e.g. 23
// This works because you get the # of days w/o hours minus UTC days without hours.
// Since it's all relative to UTC, it'll end up being -1, 0, or 1.
const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1
timezoneTupleList.push([hour, dayOffset, timezoneOffset]);
if (uniques.includes(hour)) {
duplicates.push(hour);
} else {
uniques.push(hour);
}
}
// I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
if (duplicates.includes(hour)) {
const isSameDay = await askYesOrNo(
await send(
`Is the current day of the month the ${moment().utc().format("Do")} for you?`
),
author.id
);
// Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input.
// isSameDay is checked first to reduce the amount of conditionals per loop.
if (isSameDay) {
for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) {
if (dayOffset === 0 && hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
} else {
for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) {
if (dayOffset !== 0 && hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
}
} else {
// If it's a unique hour, just search through the tuple list and find the matching entry.
for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) {
if (hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
}
// I should note that error handling should be added sometime because await throws an exception on Promise.reject.
const hasDST = await askYesOrNo(
await send("Does your timezone change based on daylight savings?"),
author.id
);
const finalize = () => {
Storage.save();
send(
"You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.",
getTimeEmbed(author)
);
};
if (hasDST) {
const finalizeDST = (region: DST) => {
profile.daylightSavingsRegion = region;
// If daylight savings is active, subtract the timezone offset by one to store the standard time.
if (hasDaylightSavings(region)) {
profile.timezone!--;
}
finalize();
};
askMultipleChoice(await send(DST_NOTE_SETUP), author.id, [
() => finalizeDST("na"),
() => finalizeDST("eu"),
() => finalizeDST("sh")
]);
} else {
finalize();
}
},
() => "you need to enter in a valid integer between 0 to 23"
30000
);
if (reply === null) return send("Message timed out.");
const hour = parseInt(reply.content);
const isValidHour = !isNaN(hour) && hour >= 0 && hour <= 23;
if (!isValidHour) return reply.reply("you need to enter in a valid integer between 0 to 23");
// You need to also take into account whether or not it's the same day in UTC or not.
// The problem this setup avoids is messing up timezones by 24 hours.
// For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
// That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days.
// (day * 24 + hour) - (day * 24 + hour)
// Since the timezones will be restricted to -12 to +14, you'll be given three options.
// The end of the month should be calculated automatically, you should have enough information at that point.
// But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day.
// 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d)
// 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d)
// 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d)
// 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d)
// For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option.
// - 23:xx same day = +0, 23:xx diff day = -1
// - 00:xx same day = +0, 00:xx diff day = +1
// - 01:xx same day = +0, 01:xx diff day = +1
// First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this:
// [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]]
// Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input.
// Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely.
// In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for.
// Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem.
// Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24
// UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12
// UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38
// Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months.
// And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms.
// That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums.
const date = new Date(); // e.g. 2021-05-01 @ 05:00
const day = date.getUTCDate(); // e.g. 1
const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29
const timezoneTupleList: [number, number, number][] = [];
const uniques: number[] = []; // only for temporary use
const duplicates = [];
// Setup the tuple list in a separate block.
for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) {
const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43)
const hour = hourSum % 24; // e.g. 23
// This works because you get the # of days w/o hours minus UTC days without hours.
// Since it's all relative to UTC, it'll end up being -1, 0, or 1.
const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1
timezoneTupleList.push([hour, dayOffset, timezoneOffset]);
if (uniques.includes(hour)) {
duplicates.push(hour);
} else {
uniques.push(hour);
}
}
// I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
if (duplicates.includes(hour)) {
const isSameDay = await confirm(
await send(`Is the current day of the month the ${moment().utc().format("Do")} for you?`),
author.id
);
// Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input.
// isSameDay is checked first to reduce the amount of conditionals per loop.
if (isSameDay) {
for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) {
if (dayOffset === 0 && hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
} else {
for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) {
if (dayOffset !== 0 && hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
}
} else {
// If it's a unique hour, just search through the tuple list and find the matching entry.
for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) {
if (hour === hourPoint) {
profile.timezone = timezoneOffset;
}
}
}
// I should note that error handling should be added sometime because await throws an exception on Promise.reject.
const hasDST = await confirm(
await send("Does your timezone change based on daylight savings?"),
author.id
);
const finalize = () => {
Storage.save();
send(
"You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.",
getTimeEmbed(author)
);
};
if (hasDST) {
const finalizeDST = (region: DST) => {
profile.daylightSavingsRegion = region;
// If daylight savings is active, subtract the timezone offset by one to store the standard time.
if (hasDaylightSavings(region)) {
profile.timezone!--;
}
finalize();
};
const index = await askMultipleChoice(await send(DST_NOTE_SETUP), author.id, 3);
switch (index) {
case 0:
finalizeDST("na");
break;
case 1:
finalizeDST("eu");
break;
case 2:
finalizeDST("sh");
break;
}
} else {
finalize();
}
return;
}
}),
delete: new NamedCommand({
description: "Delete your timezone information.",
async run({send, channel, author}) {
prompt(
await send(
"Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*"
),
author.id,
() => {
const profile = Storage.getUser(author.id);
profile.timezone = null;
profile.daylightSavingsRegion = null;
Storage.save();
}
async run({send, author}) {
const result = await confirm(
await send("Are you sure you want to delete your timezone information?"),
author.id
);
if (result) {
const profile = Storage.getUser(author.id);
profile.timezone = null;
profile.daylightSavingsRegion = null;
Storage.save();
}
}
}),
utc: new NamedCommand({
description: "Displays UTC time.",
async run({send, channel}) {
async run({send}) {
const time = moment().utc();
send({
@ -386,15 +378,15 @@ export default new NamedCommand({
id: "user",
user: new Command({
description: "See what time it is for someone else.",
async run({send, channel, args}) {
async run({send, args}) {
send(getTimeEmbed(args[0]));
}
}),
any: new RestCommand({
description: "See what time it is for someone else (by their username).",
async run({send, channel, args, guild, combined}) {
async run({send, guild, combined}) {
const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) send(getTimeEmbed(member.user));
if (typeof member !== "string") send(getTimeEmbed(member.user));
else send(member);
}
})

View File

@ -11,7 +11,7 @@ import {
GuildChannel,
Channel
} from "discord.js";
import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SingleMessageOptions, SendFunction} from "./libd";
import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SendFunction} from "./libd";
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
import {getPrefix} from "./interface";
import {parseVars, requireAllCasesHandledFor} from "../lib";
@ -244,18 +244,14 @@ export class Command extends BaseCommand {
}
// Go through the arguments provided and find the right subcommand, then execute with the given arguments.
// Will return null if it successfully executes, SingleMessageOptions if there's an error (to let the user know what it is).
// Will return null if it successfully executes, string if there's an error (to let the user know what it is).
//
// Calls the resulting subcommand's execute method in order to make more modular code, basically pushing the chain of execution to the subcommand.
// For example, a numeric subcommand would accept args of [4] then execute on it.
//
// Because each Command instance is isolated from others, it becomes practically impossible to predict the total amount of subcommands when isolating the code to handle each individual layer of recursion.
// Therefore, if a Command is declared as a rest type, any typed args that come at the end must be handled manually.
public async execute(
args: string[],
menu: CommandMenu,
metadata: ExecuteCommandMetadata
): Promise<SingleMessageOptions | null> {
public async execute(args: string[], menu: CommandMenu, metadata: ExecuteCommandMetadata): Promise<string | null> {
// Update inherited properties if the current command specifies a property.
// In case there are no initial arguments, these should go first so that it can register.
if (this.permission !== -1) metadata.permission = this.permission;
@ -292,9 +288,7 @@ export class Command extends BaseCommand {
const errorMessage = error.stack ?? error;
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
return {
content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``
};
return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
}
}
@ -313,15 +307,13 @@ export class Command extends BaseCommand {
const id = patterns.channel.exec(param)![1];
const channel = await getChannelByID(id);
if (channel instanceof Channel) {
if (typeof channel !== "string") {
if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>");
menu.args.push(channel);
return this.channel.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` is not a valid text channel!`
};
return `\`${id}\` is not a valid text channel!`;
}
} else {
return channel;
@ -330,9 +322,7 @@ export class Command extends BaseCommand {
const id = patterns.role.exec(param)![1];
if (!menu.guild) {
return {
content: "You can't use role parameters in DM channels!"
};
return "You can't use role parameters in DM channels!";
}
const role = menu.guild.roles.cache.get(id);
@ -342,9 +332,7 @@ export class Command extends BaseCommand {
menu.args.push(role);
return this.role.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` is not a valid role in this server!`
};
return `\`${id}\` is not a valid role in this server!`;
}
} else if (this.emote && patterns.emote.test(param)) {
const id = patterns.emote.exec(param)![1];
@ -355,9 +343,7 @@ export class Command extends BaseCommand {
menu.args.push(emote);
return this.emote.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` isn't a valid emote!`
};
return `\`${id}\` isn't a valid emote!`;
}
} else if (this.message && (isMessageLink || isMessagePair)) {
let channelID = "";
@ -375,7 +361,7 @@ export class Command extends BaseCommand {
const message = await getMessageByID(channelID, messageID);
if (message instanceof Message) {
if (typeof message !== "string") {
metadata.symbolicArgs.push("<message>");
menu.args.push(message);
return this.message.execute(args, menu, metadata);
@ -386,7 +372,7 @@ export class Command extends BaseCommand {
const id = patterns.user.exec(param)![1];
const user = await getUserByID(id);
if (user instanceof User) {
if (typeof user !== "string") {
metadata.symbolicArgs.push("<user>");
menu.args.push(user);
return this.user.execute(args, menu, metadata);
@ -403,24 +389,20 @@ export class Command extends BaseCommand {
case "channel":
const channel = await getChannelByID(id);
if (channel instanceof Channel) {
if (typeof channel !== "string") {
if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>");
menu.args.push(channel);
return this.id.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` is not a valid text channel!`
};
return `\`${id}\` is not a valid text channel!`;
}
} else {
return channel;
}
case "role":
if (!menu.guild) {
return {
content: "You can't use role parameters in DM channels!"
};
return "You can't use role parameters in DM channels!";
}
const role = menu.guild.roles.cache.get(id);
@ -429,9 +411,7 @@ export class Command extends BaseCommand {
menu.args.push(role);
return this.id.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` isn't a valid role in this server!`
};
return `\`${id}\` isn't a valid role in this server!`;
}
case "emote":
const emote = menu.client.emojis.cache.get(id);
@ -440,14 +420,12 @@ export class Command extends BaseCommand {
menu.args.push(emote);
return this.id.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` isn't a valid emote!`
};
return `\`${id}\` isn't a valid emote!`;
}
case "message":
const message = await getMessageByID(menu.channel, id);
if (message instanceof Message) {
if (typeof message !== "string") {
menu.args.push(message);
return this.id.execute(args, menu, metadata);
} else {
@ -456,7 +434,7 @@ export class Command extends BaseCommand {
case "user":
const user = await getUserByID(id);
if (user instanceof User) {
if (typeof user !== "string") {
menu.args.push(user);
return this.id.execute(args, menu, metadata);
} else {
@ -465,7 +443,7 @@ export class Command extends BaseCommand {
case "guild":
const guild = getGuildByID(id);
if (guild instanceof Guild) {
if (typeof guild !== "string") {
menu.args.push(guild);
return this.id.execute(args, menu, metadata);
} else {
@ -489,11 +467,9 @@ export class Command extends BaseCommand {
return this.any.execute(args.join(" "), menu, metadata);
} else {
metadata.symbolicArgs.push(`"${param}"`);
return {
content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
" "
)}\` found.`
};
return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
" "
)}\` found.`;
}
// Note: Do NOT add a return statement here. In case one of the other sections is missing
@ -682,7 +658,7 @@ export class RestCommand extends BaseCommand {
combined: string,
menu: CommandMenu,
metadata: ExecuteCommandMetadata
): Promise<SingleMessageOptions | null> {
): Promise<string | null> {
// Update inherited properties if the current command specifies a property.
// In case there are no initial arguments, these should go first so that it can register.
if (this.permission !== -1) metadata.permission = this.permission;
@ -716,9 +692,7 @@ export class RestCommand extends BaseCommand {
const errorMessage = error.stack ?? error;
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
return {
content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``
};
return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
}
}
@ -743,36 +717,34 @@ export class RestCommand extends BaseCommand {
// See if there is anything that'll prevent the user from executing the command.
// Returns null if successful, otherwise returns a message with the error.
function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): SingleMessageOptions | null {
function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): string | null {
// 1. Does this command specify a required channel type? If so, does the channel type match?
if (
metadata.channelType === CHANNEL_TYPE.GUILD &&
(!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null)
) {
return {content: "This command must be executed in a server."};
return "This command must be executed in a server.";
} else if (
metadata.channelType === CHANNEL_TYPE.DM &&
(menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null)
) {
return {content: "This command must be executed as a direct message."};
return "This command must be executed as a direct message.";
}
// 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.)
if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) {
return {content: "This command must be executed in either an NSFW channel or as a direct message."};
return "This command must be executed in either an NSFW channel or as a direct message.";
}
// 3. Does the user have permission to execute the command?
if (!hasPermission(menu.author, menu.member, metadata.permission)) {
const userPermLevel = getPermissionLevel(menu.author, menu.member);
return {
content: `You don't have access to this command! Your permission level is \`${getPermissionName(
userPermLevel
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
metadata.permission
)}\` (${metadata.permission}).`
};
return `You don't have access to this command! Your permission level is \`${getPermissionName(
userPermLevel
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
metadata.permission
)}\` (${metadata.permission}).`;
}
return null;

View File

@ -15,7 +15,9 @@ import {
MessageAdditions,
SplitOptions,
APIMessage,
StringResolvable
StringResolvable,
EmojiIdentifierResolvable,
MessageReaction
} from "discord.js";
import {unreactEventListeners, replyEventListeners} from "./eventListeners";
import {client} from "./interface";
@ -31,19 +33,6 @@ export type SendFunction = ((
((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) &
((content: StringResolvable, options: MessageOptions) => Promise<Message | Message[]>);
/**
* Tests if a bot has a certain permission in a specified guild.
*/
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
}
// The SoonTM Section //
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
// It's probably a good idea to modularize the base reaction handler so there's less copy pasted code.
// Maybe also make a reaction handler that listens for when reactions are added and removed.
// The reaction handler would also run an async function to react in order (parallel to the reaction handler).
const FIVE_BACKWARDS_EMOJI = "⏪";
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
@ -56,34 +45,35 @@ const FIVE_FORWARDS_EMOJI = "⏩";
*/
export async function paginate(
send: SendFunction,
senderID: string,
total: number,
callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
totalPages: number,
listenTo: string | null = null,
duration = 60000
) {
const hasMultiplePages = total > 1;
const message = await send(callback(0, hasMultiplePages));
): Promise<void> {
const hasMultiplePages = totalPages > 1;
const message = await send(onTurnPage(0, hasMultiplePages));
if (hasMultiplePages) {
let page = 0;
const turn = (amount: number) => {
page += amount;
if (page >= total) {
page %= total;
if (page >= totalPages) {
page %= totalPages;
} else if (page < 0) {
// Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0.
const flattened = Math.abs(page) % total;
if (flattened !== 0) page = total - flattened;
const flattened = Math.abs(page) % totalPages;
if (flattened !== 0) page = totalPages - flattened;
}
message.edit(callback(page, true));
message.edit(onTurnPage(page, true));
};
const handle = (emote: string, reacterID: string) => {
if (senderID === reacterID) {
if (reacterID === listenTo || listenTo === null) {
collector.resetTimer(); // The timer refresh MUST be present in both react and unreact.
switch (emote) {
case FIVE_BACKWARDS_EMOJI:
if (total > 5) turn(-5);
if (totalPages > 5) turn(-5);
break;
case BACKWARDS_EMOJI:
turn(-1);
@ -92,28 +82,28 @@ export async function paginate(
turn(1);
break;
case FIVE_FORWARDS_EMOJI:
if (total > 5) turn(5);
if (totalPages > 5) turn(5);
break;
}
}
};
// Listen for reactions and call the handler.
let backwardsReactionFive = total > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null;
let backwardsReactionFive = totalPages > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null;
let backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_EMOJI);
let forwardsReactionFive = total > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null;
let forwardsReactionFive = totalPages > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null;
unreactEventListeners.set(message.id, handle);
const collector = message.createReactionCollector(
(reaction, user) => {
if (user.id === senderID) {
// This check is actually redundant because of handle().
if (user.id === listenTo || listenTo === null) {
// The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
// This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES);
handle(reaction.emoji.name, user.id);
if (canDeleteEmotes) reaction.users.remove(user);
collector.resetTimer();
}
return false;
@ -134,100 +124,73 @@ export async function paginate(
}
}
// Waits for the sender to either confirm an action or let it pass (and delete the message).
// This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere.
// Append "\n*(This message will automatically be deleted after 10 seconds.)*" in the future?
/**
* Prompts the user about a decision before following through.
*/
export async function prompt(message: Message, senderID: string, onConfirm: () => void, duration = 10000) {
let isDeleted = false;
//export function generateMulti
// paginate after generateonetimeprompt
message.react("✅");
await message.awaitReactions(
(reaction, user) => {
if (user.id === senderID) {
if (reaction.emoji.name === "✅") {
onConfirm();
isDeleted = true;
message.delete();
}
}
// CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions.
return false;
},
{time: duration}
);
if (!isDeleted) message.delete();
}
// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
// If the reply is rejected, reply with an error message (when stable support comes from discord.js).
// Append "\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" in the future? And also the "you can now reply to this message" edit.
export function ask(
// Returns null if timed out, otherwise, returns the value.
export function generateOneTimePrompt<T>(
message: Message,
senderID: string,
condition: (reply: string) => boolean,
onSuccess: () => void,
onReject: () => string,
timeout = 60000
) {
const referenceID = `${message.channel.id}-${message.id}`;
stack: {[emote: string]: T},
listenTo: string | null = null,
duration = 60000
): Promise<T | null> {
return new Promise(async (resolve) => {
// First, start reacting to the message in order.
reactInOrder(message, Object.keys(stack));
replyEventListeners.set(referenceID, (reply) => {
if (reply.author.id === senderID) {
if (condition(reply.content)) {
onSuccess();
replyEventListeners.delete(referenceID);
} else {
reply.reply(onReject());
}
}
});
setTimeout(() => {
replyEventListeners.set(referenceID, (reply) => {
reply.reply("that action timed out, try using the command again");
replyEventListeners.delete(referenceID);
});
}, timeout);
}
export function askYesOrNo(message: Message, senderID: string, timeout = 30000): Promise<boolean> {
return new Promise(async (resolve, reject) => {
let isDeleted = false;
await message.react("✅");
message.react("❌");
// Then setup the reaction listener in parallel.
await message.awaitReactions(
(reaction, user) => {
if (user.id === senderID) {
const isCheckReacted = reaction.emoji.name === "✅";
(reaction: MessageReaction, user: User) => {
if (user.id === listenTo || listenTo === null) {
const emote = reaction.emoji.name;
if (isCheckReacted || reaction.emoji.name === "❌") {
resolve(isCheckReacted);
isDeleted = true;
if (emote in stack) {
resolve(stack[emote]);
message.delete();
}
}
// CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions.
return false;
},
{time: timeout}
{time: duration}
);
if (!isDeleted) {
if (!message.deleted) {
message.delete();
reject("Prompt timed out.");
resolve(null);
}
});
}
// Start a parallel chain of ordered reactions, allowing a collector to end early.
// Check if the collector ended early by seeing if the message is already deleted.
// Though apparently, message.deleted doesn't seem to update fast enough, so just put a try catch block on message.react().
async function reactInOrder(message: Message, emotes: EmojiIdentifierResolvable[]): Promise<void> {
for (const emote of emotes) {
try {
await message.react(emote);
} catch {
return;
}
}
}
export function confirm(message: Message, senderID: string, timeout = 30000): Promise<boolean | null> {
return generateOneTimePrompt(
message,
{
"✅": true,
"❌": false
},
senderID,
timeout
);
}
// This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
const multiNumbers = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"];
@ -236,40 +199,47 @@ const multiNumbers = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6
export async function askMultipleChoice(
message: Message,
senderID: string,
callbackStack: (() => void)[],
choices: number,
timeout = 90000
) {
if (callbackStack.length > multiNumbers.length) {
message.channel.send(
`\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\``
): Promise<number | null> {
if (choices > multiNumbers.length)
throw new Error(
`askMultipleChoice only accepts up to ${multiNumbers.length} options, ${choices} was provided.`
);
return;
}
const numbers: {[emote: string]: number} = {};
for (let i = 0; i < choices; i++) numbers[multiNumbers[i]] = i;
return generateOneTimePrompt(message, numbers, senderID, timeout);
}
let isDeleted = false;
// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
// If the reply is rejected, reply with an error message (when stable support comes from discord.js).
export function askForReply(message: Message, listenTo: string, timeout?: number): Promise<Message | null> {
return new Promise((resolve) => {
const referenceID = `${message.channel.id}-${message.id}`;
for (let i = 0; i < callbackStack.length; i++) {
await message.react(multiNumbers[i]);
}
await message.awaitReactions(
(reaction, user) => {
if (user.id === senderID) {
const index = multiNumbers.indexOf(reaction.emoji.name);
if (index !== -1) {
callbackStack[index]();
isDeleted = true;
message.delete();
}
replyEventListeners.set(referenceID, (reply) => {
if (reply.author.id === listenTo) {
message.delete();
replyEventListeners.delete(referenceID);
resolve(reply);
}
});
return false;
},
{time: timeout}
);
if (timeout) {
client.setTimeout(() => {
if (!message.deleted) message.delete();
replyEventListeners.delete(referenceID);
resolve(null);
}, timeout);
}
});
}
if (!isDeleted) message.delete();
/**
* Tests if a bot has a certain permission in a specified guild.
*/
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
}
// For "get x by y" methods:
@ -277,79 +247,75 @@ export async function askMultipleChoice(
// It's more reliable to get users/members by fetching their IDs. fetch() will searching through the cache anyway.
// For guilds, do an extra check to make sure there isn't an outage (guild.available).
export function getGuildByID(id: string): Guild | SingleMessageOptions {
export function getGuildByID(id: string): Guild | string {
const guild = client.guilds.cache.get(id);
if (guild) {
if (guild.available) return guild;
else return {content: `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`};
else return `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`;
} else {
return {
content: `No guild found by the ID of \`${id}\`!`
};
return `No guild found by the ID of \`${id}\`!`;
}
}
export function getGuildByName(name: string): Guild | SingleMessageOptions {
export function getGuildByName(name: string): Guild | string {
const query = name.toLowerCase();
const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) {
if (guild.available) return guild;
else return {content: `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`};
else return `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`;
} else {
return {
content: `No guild found by the name of \`${name}\`!`
};
return `No guild found by the name of \`${name}\`!`;
}
}
export async function getChannelByID(id: string): Promise<Channel | SingleMessageOptions> {
export async function getChannelByID(id: string): Promise<Channel | string> {
try {
return await client.channels.fetch(id);
} catch {
return {content: `No channel found by the ID of \`${id}\`!`};
return `No channel found by the ID of \`${id}\`!`;
}
}
// Only go through the cached channels (non-DM channels). Plus, searching DM channels by name wouldn't really make sense, nor do they have names to search anyway.
export function getChannelByName(name: string): GuildChannel | SingleMessageOptions {
export function getChannelByName(name: string): GuildChannel | string {
const query = name.toLowerCase();
const channel = client.channels.cache.find(
(channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query)
) as GuildChannel | undefined;
if (channel) return channel;
else return {content: `No channel found by the name of \`${name}\`!`};
else return `No channel found by the name of \`${name}\`!`;
}
export async function getMessageByID(
channel: TextChannel | DMChannel | NewsChannel | string,
id: string
): Promise<Message | SingleMessageOptions> {
): Promise<Message | string> {
if (typeof channel === "string") {
const targetChannel = await getChannelByID(channel);
if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel;
else if (targetChannel instanceof Channel) return {content: `\`${id}\` isn't a valid text-based channel!`};
else if (targetChannel instanceof Channel) return `\`${id}\` isn't a valid text-based channel!`;
else return targetChannel;
}
try {
return await channel.messages.fetch(id);
} catch {
return {content: `\`${id}\` isn't a valid message of the channel ${channel}!`};
return `\`${id}\` isn't a valid message of the channel ${channel}!`;
}
}
export async function getUserByID(id: string): Promise<User | SingleMessageOptions> {
export async function getUserByID(id: string): Promise<User | string> {
try {
return await client.users.fetch(id);
} catch {
return {content: `No user found by the ID of \`${id}\`!`};
return `No user found by the ID of \`${id}\`!`;
}
}
// Also check tags (if provided) to narrow down users.
export function getUserByName(name: string): User | SingleMessageOptions {
export function getUserByName(name: string): User | string {
let query = name.toLowerCase();
const tagMatch = /^(.+?)#(\d{4})$/.exec(name);
let tag: string | null = null;
@ -366,19 +332,19 @@ export function getUserByName(name: string): User | SingleMessageOptions {
});
if (user) return user;
else return {content: `No user found by the name of \`${name}\`!`};
else return `No user found by the name of \`${name}\`!`;
}
export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | SingleMessageOptions> {
export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | string> {
try {
return await guild.members.fetch(id);
} catch {
return {content: `No member found by the ID of \`${id}\`!`};
return `No member found by the ID of \`${id}\`!`;
}
}
// First checks if a member can be found by that nickname, then check if a member can be found by that username.
export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | SingleMessageOptions> {
export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | string> {
const member = (
await guild.members.fetch({
query: name,
@ -395,9 +361,9 @@ export async function getMemberByName(guild: Guild, name: string): Promise<Guild
if (user instanceof User) {
const member = guild.members.resolve(user);
if (member) return member;
else return {content: `The user \`${user.tag}\` isn't in this guild!`};
else return `The user \`${user.tag}\` isn't in this guild!`;
} else {
return {content: `No member found by the name of \`${name}\`!`};
return `No member found by the name of \`${name}\`!`;
}
}
}

View File

@ -1,5 +1,5 @@
import {client} from "../index";
import {Message, MessageEmbed} from "discord.js";
import {MessageEmbed} from "discord.js";
import {getPrefix} from "../structures";
import {getMessageByID} from "../core";
@ -13,7 +13,7 @@ client.on("message", async (message) => {
const linkMessage = await getMessageByID(channelID, messageID);
// If it's an invalid link (or the bot doesn't have access to it).
if (!(linkMessage instanceof Message)) {
if (typeof linkMessage === "string") {
return message.channel.send("I don't have access to that channel!");
}