Updated library functions
This commit is contained in:
parent
3798c27df9
commit
c980a182f8
|
@ -75,27 +75,24 @@ Because versions are assigned to batches of changes rather than single changes (
|
||||||
```ts
|
```ts
|
||||||
const pages = ["one", "two", "three"];
|
const pages = ["one", "two", "three"];
|
||||||
|
|
||||||
paginate(channel, author.id, pages.length, (page) => {
|
paginate(send, page => {
|
||||||
return {
|
return {content: pages[page]};
|
||||||
content: pages[page]
|
}, pages.length, author.id);
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`prompt()`
|
`confirm()`
|
||||||
```ts
|
```ts
|
||||||
const msg = await channel.send('Are you sure you want to delete this?');
|
const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null
|
||||||
|
|
||||||
prompt(msg, author.id, () => {
|
|
||||||
//...
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`callMemberByUsername()`
|
`askMultipleChoice()`
|
||||||
```ts
|
```ts
|
||||||
callMemberByUsername(message, args.join(" "), (member) => {
|
const result = await askMultipleChoice(await send("Which of the following numbers is your favorite?"), author.id, 4, 10000); // number (0 to 3) | null
|
||||||
channel.send(`Your nickname is ${member.nickname}.`);
|
```
|
||||||
});
|
|
||||||
|
`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
|
## [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 {BuyCommand, ShopCommand} from "./modules/eco-shop";
|
||||||
import {MondayCommand, AwardCommand} from "./modules/eco-extras";
|
import {MondayCommand, AwardCommand} from "./modules/eco-extras";
|
||||||
import {BetCommand} from "./modules/eco-bet";
|
import {BetCommand} from "./modules/eco-bet";
|
||||||
import {GuildMember} from "discord.js";
|
|
||||||
|
|
||||||
export default new NamedCommand({
|
export default new NamedCommand({
|
||||||
description: "Economy command for Monika.",
|
description: "Economy command for Monika.",
|
||||||
|
@ -38,7 +37,7 @@ export default new NamedCommand({
|
||||||
async run({send, guild, channel, args, message, combined}) {
|
async run({send, guild, channel, args, message, combined}) {
|
||||||
if (isAuthorized(guild, channel)) {
|
if (isAuthorized(guild, channel)) {
|
||||||
const member = await getMemberByName(guild!, combined);
|
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);
|
else send(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Command, NamedCommand, askYesOrNo} from "../../../core";
|
import {Command, NamedCommand, confirm} from "../../../core";
|
||||||
import {pluralise} from "../../../lib";
|
import {pluralise} from "../../../lib";
|
||||||
import {Storage} from "../../../structures";
|
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";
|
import {User} from "discord.js";
|
||||||
|
|
||||||
export const BetCommand = new NamedCommand({
|
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}`);
|
return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`);
|
||||||
|
|
||||||
// Ask target whether or not they want to take the bet.
|
// Ask target whether or not they want to take the bet.
|
||||||
const takeBet = await askYesOrNo(
|
const takeBet = await confirm(
|
||||||
await send(
|
await send(
|
||||||
`<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}`
|
`<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}`
|
||||||
),
|
),
|
||||||
target.id
|
target.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (takeBet) {
|
if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.id}>`);
|
||||||
// [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.
|
// [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
|
||||||
await send(
|
// Remove amount money from both parts at the start to avoid duplication of money.
|
||||||
`<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise(
|
sender.money -= amount;
|
||||||
amount,
|
receiver.money -= amount;
|
||||||
"Mon",
|
// Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
|
||||||
"s"
|
sender.ecoBetInsurance += amount;
|
||||||
)} has been deducted from each of them.`
|
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.
|
// Filter reactions to only collect the pertinent ones.
|
||||||
return client.setTimeout(async () => {
|
voteMsg
|
||||||
// In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
|
.awaitReactions(
|
||||||
const sender = Storage.getUser(author.id);
|
(reaction, user) => {
|
||||||
const receiver = Storage.getUser(target.id);
|
return ["✅", "❌"].includes(reaction.emoji.name);
|
||||||
// [TODO: when D.JSv13 comes out, inline reply to clean up.]
|
},
|
||||||
// When bet is over, give a vote to ask people their thoughts.
|
// [Pertinence to make configurable on the fly.]
|
||||||
const voteMsg = await send(
|
{time: parseDuration("2m")}
|
||||||
`VOTE: do you think that <@${
|
)
|
||||||
target.id
|
.then((reactions) => {
|
||||||
}> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
|
// Count votes
|
||||||
message.id
|
const okReaction = reactions.get("✅");
|
||||||
}`
|
const noReaction = reactions.get("❌");
|
||||||
);
|
const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0;
|
||||||
await voteMsg.react("✅");
|
const no = noReaction ? (noReaction.count ?? 1) - 1 : 0;
|
||||||
await voteMsg.react("❌");
|
|
||||||
|
|
||||||
// Filter reactions to only collect the pertinent ones.
|
if (ok > no) {
|
||||||
voteMsg
|
receiver.money += amount * 2;
|
||||||
.awaitReactions(
|
send(
|
||||||
(reaction, user) => {
|
`By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.`
|
||||||
return ["✅", "❌"].includes(reaction.emoji.name);
|
);
|
||||||
},
|
} else if (ok < no) {
|
||||||
// [Pertinence to make configurable on the fly.]
|
sender.money += amount * 2;
|
||||||
{time: parseDuration("2m")}
|
send(
|
||||||
)
|
`By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.`
|
||||||
.then((reactions) => {
|
);
|
||||||
// Count votes
|
} else {
|
||||||
const okReaction = reactions.get("✅");
|
sender.money += amount;
|
||||||
const noReaction = reactions.get("❌");
|
receiver.money += amount;
|
||||||
const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0;
|
send(
|
||||||
const no = noReaction ? (noReaction.count ?? 1) - 1 : 0;
|
`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) {
|
sender.ecoBetInsurance -= amount;
|
||||||
receiver.money += amount * 2;
|
receiver.ecoBetInsurance -= amount;
|
||||||
send(
|
Storage.save();
|
||||||
`By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.`
|
});
|
||||||
);
|
}, duration);
|
||||||
} 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}>`);
|
|
||||||
} else return;
|
} else return;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {GuildMember} from "discord.js";
|
import {Command, getMemberByName, NamedCommand, confirm, RestCommand} from "../../../core";
|
||||||
import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core";
|
|
||||||
import {pluralise} from "../../../lib";
|
import {pluralise} from "../../../lib";
|
||||||
import {Storage} from "../../../structures";
|
import {Storage} from "../../../structures";
|
||||||
import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils";
|
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);
|
const user = await client.users.fetch(id);
|
||||||
|
|
||||||
fields.push({
|
fields.push({
|
||||||
name: `#${i + 1}. ${user.username}#${user.discriminator}`,
|
name: `#${i + 1}. ${user.tag}`,
|
||||||
value: pluralise(users[id].money, "Mon", "s")
|
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!");
|
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);
|
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.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!");
|
else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!");
|
||||||
|
|
||||||
const target = member.user;
|
const target = member.user;
|
||||||
|
|
||||||
return prompt(
|
const result = await confirm(
|
||||||
await send(
|
await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, {
|
||||||
`Are you sure you want to send ${pluralise(
|
embed: {
|
||||||
amount,
|
color: ECO_EMBED_COLOR,
|
||||||
"Mon",
|
author: {
|
||||||
"s"
|
name: target.tag,
|
||||||
)} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`,
|
icon_url: target.displayAvatarURL({
|
||||||
{
|
format: "png",
|
||||||
embed: {
|
dynamic: true
|
||||||
color: ECO_EMBED_COLOR,
|
})
|
||||||
author: {
|
|
||||||
name: `${target.username}#${target.discriminator}`,
|
|
||||||
icon_url: target.displayAvatarURL({
|
|
||||||
format: "png",
|
|
||||||
dynamic: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
author.id,
|
author.id
|
||||||
() => {
|
|
||||||
const receiver = Storage.getUser(target.id);
|
|
||||||
sender.money -= amount;
|
|
||||||
receiver.money += amount;
|
|
||||||
Storage.save();
|
|
||||||
send(getSendEmbed(author, target, amount));
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 shopPages = split(ShopItems, 5);
|
||||||
const pageAmount = shopPages.length;
|
const pageAmount = shopPages.length;
|
||||||
|
|
||||||
paginate(send, author.id, pageAmount, (page, hasMultiplePages) => {
|
paginate(
|
||||||
return getShopEmbed(
|
send,
|
||||||
shopPages[page],
|
(page, hasMultiplePages) => {
|
||||||
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
|
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()}!`,
|
description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: `Sender: ${sender.username}#${sender.discriminator}`,
|
name: `Sender: ${sender.tag}`,
|
||||||
value: pluralise(Storage.getUser(sender.id).money, "Mon", "s")
|
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")
|
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";
|
import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core";
|
||||||
|
|
||||||
// Quotes must be used here or the numbers will change
|
// 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}) {
|
async run({send, message, channel, guild, author, client, args, combined}) {
|
||||||
const member = await getMemberByName(guild!, combined);
|
const member = await getMemberByName(guild!, combined);
|
||||||
|
|
||||||
if (member instanceof GuildMember) {
|
if (typeof member !== "string") {
|
||||||
if (member.id in registry) {
|
if (member.id in registry) {
|
||||||
send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`);
|
send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,16 +20,21 @@ export default new NamedCommand({
|
||||||
const commands = await getCommandList();
|
const commands = await getCommandList();
|
||||||
const categoryArray = commands.keyArray();
|
const categoryArray = commands.keyArray();
|
||||||
|
|
||||||
paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => {
|
paginate(
|
||||||
const category = categoryArray[page];
|
send,
|
||||||
const commandList = commands.get(category)!;
|
(page, hasMultiplePages) => {
|
||||||
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`;
|
const category = categoryArray[page];
|
||||||
for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`;
|
const commandList = commands.get(category)!;
|
||||||
return new MessageEmbed()
|
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`;
|
||||||
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category)
|
for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`;
|
||||||
.setDescription(output)
|
return new MessageEmbed()
|
||||||
.setColor(EMBED_COLOR);
|
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category)
|
||||||
});
|
.setDescription(output)
|
||||||
|
.setColor(EMBED_COLOR);
|
||||||
|
},
|
||||||
|
categoryArray.length,
|
||||||
|
author.id
|
||||||
|
);
|
||||||
},
|
},
|
||||||
any: new Command({
|
any: new Command({
|
||||||
async run({send, message, channel, guild, author, member, client, args}) {
|
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}) {
|
async run({send, message, channel, guild, author, client, args, combined}) {
|
||||||
const member = await getMemberByName(guild!, combined);
|
const member = await getMemberByName(guild!, combined);
|
||||||
|
|
||||||
if (member instanceof GuildMember) {
|
if (typeof member !== "string") {
|
||||||
send(
|
send(
|
||||||
member.user.displayAvatarURL({
|
member.user.displayAvatarURL({
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
@ -110,7 +110,7 @@ export default new NamedCommand({
|
||||||
async run({send, message, channel, guild, author, member, client, args, combined}) {
|
async run({send, message, channel, guild, author, member, client, args, combined}) {
|
||||||
const targetGuild = getGuildByName(combined);
|
const targetGuild = getGuildByName(combined);
|
||||||
|
|
||||||
if (targetGuild instanceof Guild) {
|
if (typeof targetGuild !== "string") {
|
||||||
send(await getGuildInfo(targetGuild, guild));
|
send(await getGuildInfo(targetGuild, guild));
|
||||||
} else {
|
} else {
|
||||||
send(targetGuild);
|
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)
|
// Gather the first page (if it even exists, which it might not if there no valid emotes appear)
|
||||||
if (pages > 0) {
|
if (pages > 0) {
|
||||||
paginate(send, author.id, pages, (page, hasMultiplePages) => {
|
paginate(
|
||||||
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
|
send,
|
||||||
|
(page, hasMultiplePages) => {
|
||||||
|
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
|
||||||
|
|
||||||
let desc = "";
|
let desc = "";
|
||||||
for (const emote of sections[page]) {
|
for (const emote of sections[page]) {
|
||||||
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
|
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
|
||||||
}
|
}
|
||||||
embed.setDescription(desc);
|
embed.setDescription(desc);
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
});
|
},
|
||||||
|
pages,
|
||||||
|
author.id
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
send("No valid emotes found by that query.");
|
send("No valid emotes found by that query.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
import {
|
import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core";
|
||||||
Command,
|
|
||||||
NamedCommand,
|
|
||||||
ask,
|
|
||||||
askYesOrNo,
|
|
||||||
askMultipleChoice,
|
|
||||||
prompt,
|
|
||||||
getMemberByName,
|
|
||||||
RestCommand
|
|
||||||
} from "../../core";
|
|
||||||
import {Storage} from "../../structures";
|
import {Storage} from "../../structures";
|
||||||
import {User, GuildMember} from "discord.js";
|
import {User} from "discord.js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
const DATE_FORMAT = "D MMMM YYYY";
|
const DATE_FORMAT = "D MMMM YYYY";
|
||||||
|
@ -178,183 +169,184 @@ function getTimeEmbed(user: User) {
|
||||||
export default new NamedCommand({
|
export default new NamedCommand({
|
||||||
description: "Show others what time it is for you.",
|
description: "Show others what time it is for you.",
|
||||||
aliases: ["tz"],
|
aliases: ["tz"],
|
||||||
async run({send, channel, author}) {
|
async run({send, author}) {
|
||||||
send(getTimeEmbed(author));
|
send(getTimeEmbed(author));
|
||||||
},
|
},
|
||||||
subcommands: {
|
subcommands: {
|
||||||
// Welcome to callback hell. We hope you enjoy your stay here!
|
// Welcome to callback hell. We hope you enjoy your stay here!
|
||||||
setup: new NamedCommand({
|
setup: new NamedCommand({
|
||||||
description: "Registers your timezone information for the bot.",
|
description: "Registers your timezone information for the bot.",
|
||||||
async run({send, author, channel}) {
|
async run({send, author}) {
|
||||||
const profile = Storage.getUser(author.id);
|
const profile = Storage.getUser(author.id);
|
||||||
profile.timezone = null;
|
profile.timezone = null;
|
||||||
profile.daylightSavingsRegion = null;
|
profile.daylightSavingsRegion = null;
|
||||||
let hour: number;
|
|
||||||
|
|
||||||
ask(
|
// Parse and validate reply
|
||||||
|
const reply = await askForReply(
|
||||||
await send(
|
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!)*"
|
"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,
|
author.id,
|
||||||
(reply) => {
|
30000
|
||||||
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"
|
|
||||||
);
|
);
|
||||||
|
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({
|
delete: new NamedCommand({
|
||||||
description: "Delete your timezone information.",
|
description: "Delete your timezone information.",
|
||||||
async run({send, channel, author}) {
|
async run({send, author}) {
|
||||||
prompt(
|
const result = await confirm(
|
||||||
await send(
|
await send("Are you sure you want to delete your timezone information?"),
|
||||||
"Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*"
|
author.id
|
||||||
),
|
|
||||||
author.id,
|
|
||||||
() => {
|
|
||||||
const profile = Storage.getUser(author.id);
|
|
||||||
profile.timezone = null;
|
|
||||||
profile.daylightSavingsRegion = null;
|
|
||||||
Storage.save();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const profile = Storage.getUser(author.id);
|
||||||
|
profile.timezone = null;
|
||||||
|
profile.daylightSavingsRegion = null;
|
||||||
|
Storage.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
utc: new NamedCommand({
|
utc: new NamedCommand({
|
||||||
description: "Displays UTC time.",
|
description: "Displays UTC time.",
|
||||||
async run({send, channel}) {
|
async run({send}) {
|
||||||
const time = moment().utc();
|
const time = moment().utc();
|
||||||
|
|
||||||
send({
|
send({
|
||||||
|
@ -386,15 +378,15 @@ export default new NamedCommand({
|
||||||
id: "user",
|
id: "user",
|
||||||
user: new Command({
|
user: new Command({
|
||||||
description: "See what time it is for someone else.",
|
description: "See what time it is for someone else.",
|
||||||
async run({send, channel, args}) {
|
async run({send, args}) {
|
||||||
send(getTimeEmbed(args[0]));
|
send(getTimeEmbed(args[0]));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
any: new RestCommand({
|
any: new RestCommand({
|
||||||
description: "See what time it is for someone else (by their username).",
|
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);
|
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);
|
else send(member);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
GuildChannel,
|
GuildChannel,
|
||||||
Channel
|
Channel
|
||||||
} from "discord.js";
|
} 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 {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
|
||||||
import {getPrefix} from "./interface";
|
import {getPrefix} from "./interface";
|
||||||
import {parseVars, requireAllCasesHandledFor} from "../lib";
|
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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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(
|
public async execute(args: string[], menu: CommandMenu, metadata: ExecuteCommandMetadata): Promise<string | null> {
|
||||||
args: string[],
|
|
||||||
menu: CommandMenu,
|
|
||||||
metadata: ExecuteCommandMetadata
|
|
||||||
): Promise<SingleMessageOptions | null> {
|
|
||||||
// Update inherited properties if the current command specifies a property.
|
// 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.
|
// In case there are no initial arguments, these should go first so that it can register.
|
||||||
if (this.permission !== -1) metadata.permission = this.permission;
|
if (this.permission !== -1) metadata.permission = this.permission;
|
||||||
|
@ -292,9 +288,7 @@ export class Command extends BaseCommand {
|
||||||
const errorMessage = error.stack ?? error;
|
const errorMessage = error.stack ?? error;
|
||||||
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
|
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
|
||||||
|
|
||||||
return {
|
return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
|
||||||
content: `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 id = patterns.channel.exec(param)![1];
|
||||||
const channel = await getChannelByID(id);
|
const channel = await getChannelByID(id);
|
||||||
|
|
||||||
if (channel instanceof Channel) {
|
if (typeof channel !== "string") {
|
||||||
if (channel instanceof TextChannel || channel instanceof DMChannel) {
|
if (channel instanceof TextChannel || channel instanceof DMChannel) {
|
||||||
metadata.symbolicArgs.push("<channel>");
|
metadata.symbolicArgs.push("<channel>");
|
||||||
menu.args.push(channel);
|
menu.args.push(channel);
|
||||||
return this.channel.execute(args, menu, metadata);
|
return this.channel.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` is not a valid text channel!`;
|
||||||
content: `\`${id}\` is not a valid text channel!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return channel;
|
return channel;
|
||||||
|
@ -330,9 +322,7 @@ export class Command extends BaseCommand {
|
||||||
const id = patterns.role.exec(param)![1];
|
const id = patterns.role.exec(param)![1];
|
||||||
|
|
||||||
if (!menu.guild) {
|
if (!menu.guild) {
|
||||||
return {
|
return "You can't use role parameters in DM channels!";
|
||||||
content: "You can't use role parameters in DM channels!"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = menu.guild.roles.cache.get(id);
|
const role = menu.guild.roles.cache.get(id);
|
||||||
|
@ -342,9 +332,7 @@ export class Command extends BaseCommand {
|
||||||
menu.args.push(role);
|
menu.args.push(role);
|
||||||
return this.role.execute(args, menu, metadata);
|
return this.role.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` is not a valid role in this server!`;
|
||||||
content: `\`${id}\` is not a valid role in this server!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else if (this.emote && patterns.emote.test(param)) {
|
} else if (this.emote && patterns.emote.test(param)) {
|
||||||
const id = patterns.emote.exec(param)![1];
|
const id = patterns.emote.exec(param)![1];
|
||||||
|
@ -355,9 +343,7 @@ export class Command extends BaseCommand {
|
||||||
menu.args.push(emote);
|
menu.args.push(emote);
|
||||||
return this.emote.execute(args, menu, metadata);
|
return this.emote.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` isn't a valid emote!`;
|
||||||
content: `\`${id}\` isn't a valid emote!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else if (this.message && (isMessageLink || isMessagePair)) {
|
} else if (this.message && (isMessageLink || isMessagePair)) {
|
||||||
let channelID = "";
|
let channelID = "";
|
||||||
|
@ -375,7 +361,7 @@ export class Command extends BaseCommand {
|
||||||
|
|
||||||
const message = await getMessageByID(channelID, messageID);
|
const message = await getMessageByID(channelID, messageID);
|
||||||
|
|
||||||
if (message instanceof Message) {
|
if (typeof message !== "string") {
|
||||||
metadata.symbolicArgs.push("<message>");
|
metadata.symbolicArgs.push("<message>");
|
||||||
menu.args.push(message);
|
menu.args.push(message);
|
||||||
return this.message.execute(args, menu, metadata);
|
return this.message.execute(args, menu, metadata);
|
||||||
|
@ -386,7 +372,7 @@ export class Command extends BaseCommand {
|
||||||
const id = patterns.user.exec(param)![1];
|
const id = patterns.user.exec(param)![1];
|
||||||
const user = await getUserByID(id);
|
const user = await getUserByID(id);
|
||||||
|
|
||||||
if (user instanceof User) {
|
if (typeof user !== "string") {
|
||||||
metadata.symbolicArgs.push("<user>");
|
metadata.symbolicArgs.push("<user>");
|
||||||
menu.args.push(user);
|
menu.args.push(user);
|
||||||
return this.user.execute(args, menu, metadata);
|
return this.user.execute(args, menu, metadata);
|
||||||
|
@ -403,24 +389,20 @@ export class Command extends BaseCommand {
|
||||||
case "channel":
|
case "channel":
|
||||||
const channel = await getChannelByID(id);
|
const channel = await getChannelByID(id);
|
||||||
|
|
||||||
if (channel instanceof Channel) {
|
if (typeof channel !== "string") {
|
||||||
if (channel instanceof TextChannel || channel instanceof DMChannel) {
|
if (channel instanceof TextChannel || channel instanceof DMChannel) {
|
||||||
metadata.symbolicArgs.push("<channel>");
|
metadata.symbolicArgs.push("<channel>");
|
||||||
menu.args.push(channel);
|
menu.args.push(channel);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` is not a valid text channel!`;
|
||||||
content: `\`${id}\` is not a valid text channel!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
case "role":
|
case "role":
|
||||||
if (!menu.guild) {
|
if (!menu.guild) {
|
||||||
return {
|
return "You can't use role parameters in DM channels!";
|
||||||
content: "You can't use role parameters in DM channels!"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = menu.guild.roles.cache.get(id);
|
const role = menu.guild.roles.cache.get(id);
|
||||||
|
@ -429,9 +411,7 @@ export class Command extends BaseCommand {
|
||||||
menu.args.push(role);
|
menu.args.push(role);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` isn't a valid role in this server!`;
|
||||||
content: `\`${id}\` isn't a valid role in this server!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case "emote":
|
case "emote":
|
||||||
const emote = menu.client.emojis.cache.get(id);
|
const emote = menu.client.emojis.cache.get(id);
|
||||||
|
@ -440,14 +420,12 @@ export class Command extends BaseCommand {
|
||||||
menu.args.push(emote);
|
menu.args.push(emote);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return `\`${id}\` isn't a valid emote!`;
|
||||||
content: `\`${id}\` isn't a valid emote!`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case "message":
|
case "message":
|
||||||
const message = await getMessageByID(menu.channel, id);
|
const message = await getMessageByID(menu.channel, id);
|
||||||
|
|
||||||
if (message instanceof Message) {
|
if (typeof message !== "string") {
|
||||||
menu.args.push(message);
|
menu.args.push(message);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
|
@ -456,7 +434,7 @@ export class Command extends BaseCommand {
|
||||||
case "user":
|
case "user":
|
||||||
const user = await getUserByID(id);
|
const user = await getUserByID(id);
|
||||||
|
|
||||||
if (user instanceof User) {
|
if (typeof user !== "string") {
|
||||||
menu.args.push(user);
|
menu.args.push(user);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
|
@ -465,7 +443,7 @@ export class Command extends BaseCommand {
|
||||||
case "guild":
|
case "guild":
|
||||||
const guild = getGuildByID(id);
|
const guild = getGuildByID(id);
|
||||||
|
|
||||||
if (guild instanceof Guild) {
|
if (typeof guild !== "string") {
|
||||||
menu.args.push(guild);
|
menu.args.push(guild);
|
||||||
return this.id.execute(args, menu, metadata);
|
return this.id.execute(args, menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
|
@ -489,11 +467,9 @@ export class Command extends BaseCommand {
|
||||||
return this.any.execute(args.join(" "), menu, metadata);
|
return this.any.execute(args.join(" "), menu, metadata);
|
||||||
} else {
|
} else {
|
||||||
metadata.symbolicArgs.push(`"${param}"`);
|
metadata.symbolicArgs.push(`"${param}"`);
|
||||||
return {
|
return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
|
||||||
content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
|
" "
|
||||||
" "
|
)}\` found.`;
|
||||||
)}\` found.`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Do NOT add a return statement here. In case one of the other sections is missing
|
// 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,
|
combined: string,
|
||||||
menu: CommandMenu,
|
menu: CommandMenu,
|
||||||
metadata: ExecuteCommandMetadata
|
metadata: ExecuteCommandMetadata
|
||||||
): Promise<SingleMessageOptions | null> {
|
): Promise<string | null> {
|
||||||
// Update inherited properties if the current command specifies a property.
|
// 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.
|
// In case there are no initial arguments, these should go first so that it can register.
|
||||||
if (this.permission !== -1) metadata.permission = this.permission;
|
if (this.permission !== -1) metadata.permission = this.permission;
|
||||||
|
@ -716,9 +692,7 @@ export class RestCommand extends BaseCommand {
|
||||||
const errorMessage = error.stack ?? error;
|
const errorMessage = error.stack ?? error;
|
||||||
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
|
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
|
||||||
|
|
||||||
return {
|
return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
|
||||||
content: `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.
|
// 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.
|
// 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?
|
// 1. Does this command specify a required channel type? If so, does the channel type match?
|
||||||
if (
|
if (
|
||||||
metadata.channelType === CHANNEL_TYPE.GUILD &&
|
metadata.channelType === CHANNEL_TYPE.GUILD &&
|
||||||
(!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null)
|
(!(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 (
|
} else if (
|
||||||
metadata.channelType === CHANNEL_TYPE.DM &&
|
metadata.channelType === CHANNEL_TYPE.DM &&
|
||||||
(menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null)
|
(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.)
|
// 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) {
|
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?
|
// 3. Does the user have permission to execute the command?
|
||||||
if (!hasPermission(menu.author, menu.member, metadata.permission)) {
|
if (!hasPermission(menu.author, menu.member, metadata.permission)) {
|
||||||
const userPermLevel = getPermissionLevel(menu.author, menu.member);
|
const userPermLevel = getPermissionLevel(menu.author, menu.member);
|
||||||
|
|
||||||
return {
|
return `You don't have access to this command! Your permission level is \`${getPermissionName(
|
||||||
content: `You don't have access to this command! Your permission level is \`${getPermissionName(
|
userPermLevel
|
||||||
userPermLevel
|
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
|
||||||
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
|
metadata.permission
|
||||||
metadata.permission
|
)}\` (${metadata.permission}).`;
|
||||||
)}\` (${metadata.permission}).`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
288
src/core/libd.ts
288
src/core/libd.ts
|
@ -15,7 +15,9 @@ import {
|
||||||
MessageAdditions,
|
MessageAdditions,
|
||||||
SplitOptions,
|
SplitOptions,
|
||||||
APIMessage,
|
APIMessage,
|
||||||
StringResolvable
|
StringResolvable,
|
||||||
|
EmojiIdentifierResolvable,
|
||||||
|
MessageReaction
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import {unreactEventListeners, replyEventListeners} from "./eventListeners";
|
import {unreactEventListeners, replyEventListeners} from "./eventListeners";
|
||||||
import {client} from "./interface";
|
import {client} from "./interface";
|
||||||
|
@ -31,19 +33,6 @@ export type SendFunction = ((
|
||||||
((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) &
|
((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) &
|
||||||
((content: StringResolvable, options: MessageOptions) => Promise<Message | 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 FIVE_BACKWARDS_EMOJI = "⏪";
|
||||||
const BACKWARDS_EMOJI = "⬅️";
|
const BACKWARDS_EMOJI = "⬅️";
|
||||||
const FORWARDS_EMOJI = "➡️";
|
const FORWARDS_EMOJI = "➡️";
|
||||||
|
@ -56,34 +45,35 @@ const FIVE_FORWARDS_EMOJI = "⏩";
|
||||||
*/
|
*/
|
||||||
export async function paginate(
|
export async function paginate(
|
||||||
send: SendFunction,
|
send: SendFunction,
|
||||||
senderID: string,
|
onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
|
||||||
total: number,
|
totalPages: number,
|
||||||
callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
|
listenTo: string | null = null,
|
||||||
duration = 60000
|
duration = 60000
|
||||||
) {
|
): Promise<void> {
|
||||||
const hasMultiplePages = total > 1;
|
const hasMultiplePages = totalPages > 1;
|
||||||
const message = await send(callback(0, hasMultiplePages));
|
const message = await send(onTurnPage(0, hasMultiplePages));
|
||||||
|
|
||||||
if (hasMultiplePages) {
|
if (hasMultiplePages) {
|
||||||
let page = 0;
|
let page = 0;
|
||||||
const turn = (amount: number) => {
|
const turn = (amount: number) => {
|
||||||
page += amount;
|
page += amount;
|
||||||
|
|
||||||
if (page >= total) {
|
if (page >= totalPages) {
|
||||||
page %= total;
|
page %= totalPages;
|
||||||
} else if (page < 0) {
|
} 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.
|
// 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;
|
const flattened = Math.abs(page) % totalPages;
|
||||||
if (flattened !== 0) page = total - flattened;
|
if (flattened !== 0) page = totalPages - flattened;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.edit(callback(page, true));
|
message.edit(onTurnPage(page, true));
|
||||||
};
|
};
|
||||||
const handle = (emote: string, reacterID: string) => {
|
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) {
|
switch (emote) {
|
||||||
case FIVE_BACKWARDS_EMOJI:
|
case FIVE_BACKWARDS_EMOJI:
|
||||||
if (total > 5) turn(-5);
|
if (totalPages > 5) turn(-5);
|
||||||
break;
|
break;
|
||||||
case BACKWARDS_EMOJI:
|
case BACKWARDS_EMOJI:
|
||||||
turn(-1);
|
turn(-1);
|
||||||
|
@ -92,28 +82,28 @@ export async function paginate(
|
||||||
turn(1);
|
turn(1);
|
||||||
break;
|
break;
|
||||||
case FIVE_FORWARDS_EMOJI:
|
case FIVE_FORWARDS_EMOJI:
|
||||||
if (total > 5) turn(5);
|
if (totalPages > 5) turn(5);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for reactions and call the handler.
|
// 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 backwardsReaction = await message.react(BACKWARDS_EMOJI);
|
||||||
let forwardsReaction = await message.react(FORWARDS_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);
|
unreactEventListeners.set(message.id, handle);
|
||||||
|
|
||||||
const collector = message.createReactionCollector(
|
const collector = message.createReactionCollector(
|
||||||
(reaction, user) => {
|
(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.
|
// 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.
|
// 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);
|
const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES);
|
||||||
handle(reaction.emoji.name, user.id);
|
handle(reaction.emoji.name, user.id);
|
||||||
if (canDeleteEmotes) reaction.users.remove(user);
|
if (canDeleteEmotes) reaction.users.remove(user);
|
||||||
collector.resetTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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).
|
//export function generateMulti
|
||||||
// This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere.
|
// paginate after generateonetimeprompt
|
||||||
// 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;
|
|
||||||
|
|
||||||
message.react("✅");
|
// Returns null if timed out, otherwise, returns the value.
|
||||||
await message.awaitReactions(
|
export function generateOneTimePrompt<T>(
|
||||||
(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(
|
|
||||||
message: Message,
|
message: Message,
|
||||||
senderID: string,
|
stack: {[emote: string]: T},
|
||||||
condition: (reply: string) => boolean,
|
listenTo: string | null = null,
|
||||||
onSuccess: () => void,
|
duration = 60000
|
||||||
onReject: () => string,
|
): Promise<T | null> {
|
||||||
timeout = 60000
|
return new Promise(async (resolve) => {
|
||||||
) {
|
// First, start reacting to the message in order.
|
||||||
const referenceID = `${message.channel.id}-${message.id}`;
|
reactInOrder(message, Object.keys(stack));
|
||||||
|
|
||||||
replyEventListeners.set(referenceID, (reply) => {
|
// Then setup the reaction listener in parallel.
|
||||||
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("❌");
|
|
||||||
await message.awaitReactions(
|
await message.awaitReactions(
|
||||||
(reaction, user) => {
|
(reaction: MessageReaction, user: User) => {
|
||||||
if (user.id === senderID) {
|
if (user.id === listenTo || listenTo === null) {
|
||||||
const isCheckReacted = reaction.emoji.name === "✅";
|
const emote = reaction.emoji.name;
|
||||||
|
|
||||||
if (isCheckReacted || reaction.emoji.name === "❌") {
|
if (emote in stack) {
|
||||||
resolve(isCheckReacted);
|
resolve(stack[emote]);
|
||||||
isDeleted = true;
|
|
||||||
message.delete();
|
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;
|
return false;
|
||||||
},
|
},
|
||||||
{time: timeout}
|
{time: duration}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isDeleted) {
|
if (!message.deleted) {
|
||||||
message.delete();
|
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.
|
// 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️⃣", "🔟"];
|
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(
|
export async function askMultipleChoice(
|
||||||
message: Message,
|
message: Message,
|
||||||
senderID: string,
|
senderID: string,
|
||||||
callbackStack: (() => void)[],
|
choices: number,
|
||||||
timeout = 90000
|
timeout = 90000
|
||||||
) {
|
): Promise<number | null> {
|
||||||
if (callbackStack.length > multiNumbers.length) {
|
if (choices > multiNumbers.length)
|
||||||
message.channel.send(
|
throw new Error(
|
||||||
`\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\``
|
`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++) {
|
replyEventListeners.set(referenceID, (reply) => {
|
||||||
await message.react(multiNumbers[i]);
|
if (reply.author.id === listenTo) {
|
||||||
}
|
message.delete();
|
||||||
|
replyEventListeners.delete(referenceID);
|
||||||
await message.awaitReactions(
|
resolve(reply);
|
||||||
(reaction, user) => {
|
|
||||||
if (user.id === senderID) {
|
|
||||||
const index = multiNumbers.indexOf(reaction.emoji.name);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
callbackStack[index]();
|
|
||||||
isDeleted = true;
|
|
||||||
message.delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return false;
|
if (timeout) {
|
||||||
},
|
client.setTimeout(() => {
|
||||||
{time: timeout}
|
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:
|
// 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.
|
// 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).
|
// 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);
|
const guild = client.guilds.cache.get(id);
|
||||||
|
|
||||||
if (guild) {
|
if (guild) {
|
||||||
if (guild.available) return 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 {
|
} else {
|
||||||
return {
|
return `No guild found by the ID of \`${id}\`!`;
|
||||||
content: `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 query = name.toLowerCase();
|
||||||
const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
|
const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
|
||||||
|
|
||||||
if (guild) {
|
if (guild) {
|
||||||
if (guild.available) return 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 {
|
} else {
|
||||||
return {
|
return `No guild found by the name of \`${name}\`!`;
|
||||||
content: `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 {
|
try {
|
||||||
return await client.channels.fetch(id);
|
return await client.channels.fetch(id);
|
||||||
} catch {
|
} 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.
|
// 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 query = name.toLowerCase();
|
||||||
const channel = client.channels.cache.find(
|
const channel = client.channels.cache.find(
|
||||||
(channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query)
|
(channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query)
|
||||||
) as GuildChannel | undefined;
|
) as GuildChannel | undefined;
|
||||||
if (channel) return channel;
|
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(
|
export async function getMessageByID(
|
||||||
channel: TextChannel | DMChannel | NewsChannel | string,
|
channel: TextChannel | DMChannel | NewsChannel | string,
|
||||||
id: string
|
id: string
|
||||||
): Promise<Message | SingleMessageOptions> {
|
): Promise<Message | string> {
|
||||||
if (typeof channel === "string") {
|
if (typeof channel === "string") {
|
||||||
const targetChannel = await getChannelByID(channel);
|
const targetChannel = await getChannelByID(channel);
|
||||||
if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel;
|
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;
|
else return targetChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await channel.messages.fetch(id);
|
return await channel.messages.fetch(id);
|
||||||
} catch {
|
} 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 {
|
try {
|
||||||
return await client.users.fetch(id);
|
return await client.users.fetch(id);
|
||||||
} catch {
|
} 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.
|
// 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();
|
let query = name.toLowerCase();
|
||||||
const tagMatch = /^(.+?)#(\d{4})$/.exec(name);
|
const tagMatch = /^(.+?)#(\d{4})$/.exec(name);
|
||||||
let tag: string | null = null;
|
let tag: string | null = null;
|
||||||
|
@ -366,19 +332,19 @@ export function getUserByName(name: string): User | SingleMessageOptions {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) return user;
|
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 {
|
try {
|
||||||
return await guild.members.fetch(id);
|
return await guild.members.fetch(id);
|
||||||
} catch {
|
} 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.
|
// 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 = (
|
const member = (
|
||||||
await guild.members.fetch({
|
await guild.members.fetch({
|
||||||
query: name,
|
query: name,
|
||||||
|
@ -395,9 +361,9 @@ export async function getMemberByName(guild: Guild, name: string): Promise<Guild
|
||||||
if (user instanceof User) {
|
if (user instanceof User) {
|
||||||
const member = guild.members.resolve(user);
|
const member = guild.members.resolve(user);
|
||||||
if (member) return member;
|
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 {
|
} 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 {client} from "../index";
|
||||||
import {Message, MessageEmbed} from "discord.js";
|
import {MessageEmbed} from "discord.js";
|
||||||
import {getPrefix} from "../structures";
|
import {getPrefix} from "../structures";
|
||||||
import {getMessageByID} from "../core";
|
import {getMessageByID} from "../core";
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ client.on("message", async (message) => {
|
||||||
const linkMessage = await getMessageByID(channelID, messageID);
|
const linkMessage = await getMessageByID(channelID, messageID);
|
||||||
|
|
||||||
// If it's an invalid link (or the bot doesn't have access to it).
|
// 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!");
|
return message.channel.send("I don't have access to that channel!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue