mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Updated library functions
This commit is contained in:
parent
3798c27df9
commit
c980a182f8
14 changed files with 479 additions and 542 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
288
src/core/libd.ts
288
src/core/libd.ts
|
@ -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}\`!`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue