Add pager and confirmation prompt

This commit is contained in:
Snazzah 2021-06-24 00:28:26 -05:00
parent a736a0f140
commit 2aeacc0a77
No known key found for this signature in database
GPG key ID: 5E71D54F3D86282E
5 changed files with 283 additions and 0 deletions

View file

@ -36,6 +36,7 @@ module.exports = {
messageLimit: 0,
intents: [
"guilds",
"guildMembers",
"guildMessages",
"guildMessageReactions",
"directMessages",

View file

@ -21,6 +21,7 @@
"config": "^3.3.6",
"dexare": "^2.0.1",
"eventemitter3": "^4.0.7",
"lodash.chunk": "^4.2.0",
"quick.db": "^7.1.3",
"steno": "^2.0.0"
},
@ -28,6 +29,7 @@
"@types/common-tags": "^1.8.0",
"@types/config": "^0.0.38",
"@types/cron": "^1.7.2",
"@types/lodash.chunk": "^4.2.6",
"@types/needle": "^2.5.1",
"@types/node": "^15.12.4",
"@typescript-eslint/eslint-plugin": "^4.28.0",

View file

@ -1,3 +1,6 @@
import { CommandContext } from 'dexare';
import Eris from 'eris';
/**
* Make a promise that resolves after some time
* @param ms The time to resolve at
@ -70,3 +73,89 @@ export function splitMessage(
}
return messages.concat(msg).filter((m) => m);
}
export enum ConfirmEmoji {
YES = '✔️',
NO = '❌'
}
/**
* Create a confirmation prompt.
* @param ctx The context to use
* @param content The content to send
* @param file The file(s)
*/
export async function confirm(
ctx: CommandContext,
content: Eris.MessageContent,
file?: Eris.MessageFile | Eris.MessageFile[]
): Promise<boolean> {
const botUser = ctx.client.bot.user.id;
const message = await ctx.reply(content, file);
const group = `confirm:${message.id}`;
return new Promise((resolve) => {
const timeout = setTimeout(() => cb(false), 30000);
const cb = (result: boolean, destroyed = false) => {
clearTimeout(timeout);
if (!destroyed) message.delete().catch(() => {});
ctx.client.events.unregisterGroup(group);
resolve(result);
};
/* #region events */
ctx.client.events.register(group, 'messageReactionAdd', (_, { id }, emoji, member) => {
if (message.id === id && member.id === ctx.author.id) {
if (emoji.name === ConfirmEmoji.YES) cb(true);
if (emoji.name === ConfirmEmoji.NO) cb(false);
}
});
ctx.client.events.register(
group,
'messageCreate',
(event, reply) => {
if (reply.channel.id === message.channel.id && reply.author.id === ctx.author.id) {
if (reply.content.toLowerCase() === 'yes') {
event.skip('commands');
cb(true);
} else if (reply.content.toLowerCase() === 'no') {
event.skip('commands');
cb(false);
}
}
},
{ before: ['commands'] }
);
ctx.client.events.register(group, 'messageDelete', (_, { id }) => {
if (message.id === id) cb(false, true);
});
ctx.client.events.register(group, 'messageDeleteBulk', (_, messages) => {
for (const { id } of messages) if (message.id === id) return cb(false, true);
});
ctx.client.events.register(group, 'channelDelete', (_, channel) => {
if (message.channel.id === channel.id) return cb(false, true);
});
if (message.guildID) {
ctx.client.events.register(group, 'guildDelete', (_, guild) => {
if (message.guildID === guild.id) return cb(false, true);
});
ctx.client.events.register(group, 'guildMemberRemove', (_, guild, member) => {
if (message.guildID === guild.id && member.id === ctx.author.id) return cb(false);
});
ctx.client.events.register(group, 'guildBanAdd', (_, guild, user) => {
if (message.guildID === guild.id && user.id === ctx.author.id) return cb(false);
});
}
/* #endregion */
if ('permissionsOf' in ctx.channel ? ctx.channel.permissionsOf(botUser).has('addReactions') : true)
Promise.all([ConfirmEmoji.YES, ConfirmEmoji.NO].map(message.addReaction)).catch(() => {});
});
}

174
src/util/pager.ts Normal file
View file

@ -0,0 +1,174 @@
import { ClientEvent, CommandContext } from 'dexare';
import Eris from 'eris';
import chunk from 'lodash.chunk';
import { splitMessage } from '.';
export enum PagerEmoji {
STOP = '🛑',
NEXT = '➡️',
PREVIOUS = '⬅️',
FIRST = '⏮️',
LAST = '⏭️'
}
export interface PagerOptions {
items: string[];
itemsPerPage?: number | 'auto';
itemSeparator?: string;
characterLimit?: number;
idleTime?: number;
startPage?: number;
title?: string;
}
export interface InternalPagerStopOptions {
destroy?: boolean;
destroyed?: boolean;
}
export interface InternalPagerTurnOptions {
emoji?: Eris.Emoji;
event?: ClientEvent;
reply?: Eris.Message;
}
/**
* Create a pagination.
*/
export async function paginate(
ctx: CommandContext,
{
items,
itemsPerPage = 'auto',
itemSeparator = '\n',
characterLimit = 2048,
idleTime = 30000,
startPage = 1,
title = 'Items'
}: PagerOptions,
embed?: Eris.EmbedOptions
) {
/* #region pages */
const pages: string[] =
itemsPerPage === 'auto'
? splitMessage(items.join(itemSeparator), { maxLength: characterLimit })
: chunk(items, itemsPerPage).map((page) => page.join(itemSeparator));
if (isNaN(startPage) || startPage <= 1) startPage = 1;
else if (startPage > pages.length) startPage = pages.length;
let page = startPage;
const render = (page: number): Eris.MessageContent => ({
embed: {
...(embed || {}),
title: `${title} [${page}/${pages.length}]`,
description: pages[page + 1]
}
});
/* #endregion */
const botUser = ctx.client.bot.user.id;
const canReact =
'permissionsOf' in ctx.channel ? ctx.channel.permissionsOf(botUser).has('addReactions') : true;
const canManage =
'permissionsOf' in ctx.channel ? ctx.channel.permissionsOf(botUser).has('manageMessages') : false;
const message = await ctx.reply(render(page));
const group = `pager:${message.id}`;
let reacted: string[] = [];
/* #region page functions */
let idleTimeout = setTimeout(() => stop(), idleTime);
const stop = ({ destroy = false, destroyed = false }: InternalPagerStopOptions = {}) => {
clearTimeout(idleTimeout);
if (!destroyed && destroy) message.delete().catch(() => {});
// Remove reactions
if (!destroyed && !destroy) {
if (canManage) message.removeReactions().catch(() => {});
else Promise.all(reacted.map((emoji) => message.removeReaction(emoji))).catch(() => {});
}
ctx.client.events.unregisterGroup(group);
};
const turn = (toPage: number, { emoji, event, reply }: InternalPagerTurnOptions) => {
if (emoji && canManage) message.removeReaction(emoji.name, ctx.author.id).catch(() => {});
if (event) event.skip('commands');
if (reply && canManage) reply.delete().catch(() => {});
clearTimeout(idleTimeout);
idleTimeout = setTimeout(() => stop(), idleTime);
page = toPage;
message.edit(render(page)).catch(() => {});
};
/* #endregion */
/* #region events */
ctx.client.events.register(group, 'messageReactionAdd', (_, { id }, emoji, member) => {
if (message.id === id && member.id === ctx.author.id) {
if (emoji.name === PagerEmoji.FIRST && page !== 1) turn(1, { emoji });
if (emoji.name === PagerEmoji.LAST && page !== pages.length) turn(pages.length, { emoji });
if (emoji.name === PagerEmoji.NEXT && page > pages.length) turn(page + 1, { emoji });
if (emoji.name === PagerEmoji.PREVIOUS && page <= 1) turn(page - 1, { emoji });
if (emoji.name === PagerEmoji.STOP) stop({ destroy: true });
}
});
ctx.client.events.register(
group,
'messageCreate',
(event, reply) => {
if (reply.channel.id === message.channel.id && reply.author.id === ctx.author.id) {
if (['>>', 'first'].includes(reply.content.toLowerCase()) && page !== 1) turn(1, { event, reply });
if (['<<', 'last'].includes(reply.content.toLowerCase()) && page !== pages.length)
turn(pages.length, { event, reply });
if (['>', 'next', 'forward'].includes(reply.content.toLowerCase()) && page > pages.length)
turn(page + 1, { event, reply });
if (['<', 'previous', 'prev', 'back'].includes(reply.content.toLowerCase()) && page <= 1)
turn(page - 1, { event, reply });
if (reply.content.toLowerCase().startsWith('page ') && reply.content.length > 5) {
let newPage = parseInt(reply.content.slice(4).trim());
if (isNaN(startPage)) return;
if (newPage <= 1) newPage = 1;
else if (startPage > pages.length) startPage = pages.length;
if (page !== newPage) {
page = newPage;
turn(newPage, { event, reply });
}
}
}
},
{ before: ['commands'] }
);
ctx.client.events.register(group, 'messageDelete', (_, { id }) => {
if (message.id === id) stop({ destroyed: true });
});
ctx.client.events.register(group, 'messageDeleteBulk', (_, messages) => {
for (const { id } of messages) if (message.id === id) stop({ destroyed: true });
});
ctx.client.events.register(group, 'channelDelete', (_, channel) => {
if (message.channel.id === channel.id) stop({ destroyed: true });
});
if (message.guildID) {
ctx.client.events.register(group, 'guildDelete', (_, guild) => {
if (message.guildID === guild.id) stop({ destroyed: true });
});
ctx.client.events.register(group, 'guildMemberRemove', (_, guild, member) => {
if (message.guildID === guild.id && member.id === ctx.author.id) stop({ destroy: true });
});
ctx.client.events.register(group, 'guildBanAdd', (_, guild, user) => {
if (message.guildID === guild.id && user.id === ctx.author.id) stop({ destroy: true });
});
}
/* #endregion */
if (canReact && pages.length > 1) {
reacted =
pages.length === 2
? [PagerEmoji.PREVIOUS, PagerEmoji.STOP, PagerEmoji.NEXT]
: [PagerEmoji.FIRST, PagerEmoji.PREVIOUS, PagerEmoji.STOP, PagerEmoji.NEXT, PagerEmoji.LAST];
Promise.all(reacted.map(message.addReaction)).catch(() => {});
}
}

View file

@ -151,6 +151,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
"@types/lodash.chunk@^4.2.6":
version "4.2.6"
resolved "https://registry.yarnpkg.com/@types/lodash.chunk/-/lodash.chunk-4.2.6.tgz#9d35f05360b0298715d7f3d9efb34dd4f77e5d2a"
integrity sha512-SPlusB7jxXyGcTXYcUdWr7WmhArO/rmTq54VN88iKMxGUhyg79I4Q8n4riGn3kjaTjOJrVlHhxgX/d7woak5BQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.170"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/needle@^2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@types/needle/-/needle-2.5.1.tgz#2923f4a63a66048aed3038d76e8bc83b6905c784"
@ -1508,6 +1520,11 @@ lodash.camelcase@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.chunk@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
integrity sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"