Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modmail #840

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ POINT_DECAY_TIMER=24
MOD_ROLE_ID=
ADMIN_ROLE_ID=
SERVER_ID= # Local environment server ID
MODMAIL_CHANNEL_ID=
DM_ALT_CHANNEL_ID=

POINT_LIMITER_IN_MINUTES=
# Used for checking if the user has given a point to another user within the timeframe provided
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"references.preferredLocation": "view",
"editor.suggest.maxVisibleSuggestions": 5,
"editor.rulers": [80],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"eslint.packageManager": "yarn",
"search.exclude": {
Expand Down
2 changes: 2 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const {DUMMY_TOKEN} = process.env;
export const {DISCORD_TOKEN} = process.env;
export const {REPO_LINK} = process.env;

export const {MODMAIL_CHANNEL_ID} = process.env;
export const {DM_ALT_CHANNEL_ID} = process.env
export const {MOD_CHANNEL} = process.env;
export const {NUMBER_OF_ALLOWED_MESSAGES} = process.env;
export const {CACHE_REVALIDATION_IN_SECONDS} = process.env;
Expand Down
26 changes: 24 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import type {
ApplicationCommandData,
ApplicationCommandPermissionsManager,
ChatInputApplicationCommandData,
Client,
CommandInteraction,
ContextMenuInteraction,
Guild,
Interaction,
} from 'discord.js';

export type CommandDataWithHandler = ChatInputApplicationCommandData & {
handler: (client: Client, interaction: CommandInteraction) => Promise<void>;
export type CommandDataWithHandler = ApplicationCommandData & {
handler: (
client: Client,
interaction: CommandInteraction | ContextMenuInteraction
) => Promise<void>;
onAttach?: (client: Client) => void;
guildValidate?: (guild: Guild) => boolean;
managePermissions?: (
guild: Guild,
permissions: ApplicationCommandPermissionsManager<
{
guild: GuildResolvable;
},
{
guild: GuildResolvable;
},
{
guild: GuildResolvable;
},
Guild,
string
>
) => Promise<void>;
};
21 changes: 19 additions & 2 deletions src/v2/cache/cacheFns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ export async function upsert(options: CacheUpsertOptions) {
const { guild, type, user, meta } = options;

// Cannot use destructuring due to the properties being optional and TS not liking it
// eslint-disable-next-line unicorn/consistent-destructuring
const expireTime = "expiresAt" in options ? options.expiresAt : Date.now() + options.expiresIn;

const expireTime =
'expiresAt' in options ? options.expiresAt : Date.now() + options.expiresIn;

const result = await GenericCache.findOneAndUpdate(
{
Expand Down Expand Up @@ -62,3 +63,19 @@ async function _purge() {
timestamp: { $lt: Date.now() },
});
}

export async function purgeType({
guild,
type,
}: {
guild: string;
type: string;
}) {
await Promise.all([
_purge(),
GenericCache.deleteMany({
type,
guild,
}),
]);
}
105 changes: 77 additions & 28 deletions src/v2/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// we need to await in a loop as we're rate-limited anyway
/* eslint-disable no-await-in-loop */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {
ApplicationCommand,
Expand All @@ -7,12 +9,15 @@ import type {
Guild,
GuildApplicationCommandManager,
GuildResolvable,
ApplicationCommandType,
} from 'discord.js';
import { Collection } from 'discord.js';
import { ApplicationCommandTypes } from 'discord.js/typings/enums';
import { filter } from 'domyno';
import { isEqual } from 'lodash-es';

import type { CommandDataWithHandler } from '../../types';
import { modmailCommands } from '../modules/modmail';
import { asyncCatch } from '../utils/asyncCatch.js';
import { map, mapʹ } from '../utils/map.js';
import { merge } from '../utils/merge.js';
Expand Down Expand Up @@ -46,6 +51,7 @@ export const guildCommands = new Map(
shitpostInteraction,
npmInteraction,
whynoInteraction,
...modmailCommands,
// warn // Not used atm
].map(command => [command.name, command])
); // placeholder for now
Expand All @@ -58,16 +64,22 @@ export const applicationCommands = new Collection<
const getRelevantCmdProperties = ({
description,
name,
type = ApplicationCommandTypes.CHAT_INPUT,
options,
defaultPermission = true,
}: {
description: string;
type?: ApplicationCommandTypes | ApplicationCommandType;
description?: string;
name: string;
options?: unknown[];
defaultPermission?: boolean;
}): ApplicationCommandData => {
const relevantData = {
type: _normalizeType(type),
description,
name,
options,
defaultPermission,
} as unknown as ApplicationCommandData;
return stripNullish(normalizeApplicationCommandData(relevantData));
};
Expand All @@ -92,7 +104,7 @@ export const registerCommands = async (client: Client): Promise<void> => {
client.on(
'interactionCreate',
asyncCatch(async interaction => {
if (!interaction.isCommand()) {
if (!interaction.isCommand() && !interaction.isContextMenu()) {
return;
}

Expand All @@ -113,23 +125,30 @@ export const registerCommands = async (client: Client): Promise<void> => {
}
} catch (error) {
console.error(error);
await interaction.reply({
ephemeral: true,
content: 'Something went wrong when trying to execute the command',
});

if (interaction.deferred) {
await interaction.editReply({
content: 'Something went wrong when trying to execute the command',
});
} else {
await interaction.reply({
ephemeral: true,
content: 'Something went wrong when trying to execute the command',
});
}
}
})
);

for (const { onAttach } of applicationCommands.values()) {
// We're attaching these so it's fine

onAttach?.(client);
}

for (const { onAttach } of guildCommands.values()) {
// We're attaching these so it's fine

onAttach?.(client);
}

Expand Down Expand Up @@ -161,33 +180,49 @@ export const registerCommands = async (client: Client): Promise<void> => {
// })
};

function _normalizeType(
type: ApplicationCommandType | ApplicationCommandTypes
) {
if (typeof type === 'number') {
return type;
}

switch (type) {
case 'MESSAGE':
return ApplicationCommandTypes.MESSAGE;
case 'USER':
return ApplicationCommandTypes.USER;
case 'CHAT_INPUT':
default:
return ApplicationCommandTypes.CHAT_INPUT;
}
}

const interactionTypes = new Set(['CHAT_INPUT','USER','MESSAGE'])
async function addCommands(
serverCommands: Collection<
string,
ApplicationCommand<{ guild: GuildResolvable }>
ApplicationCommand
>,
commandDescriptions: Map<string, CommandDataWithHandler>,
commandManager: ApplicationCommandManager | GuildApplicationCommandManager
) {
const discordChatInputCommandsById = serverCommands.filter(
x => x.type === 'CHAT_INPUT'
const discordInteractionsById = serverCommands.filter(
x => interactionTypes.has(x.type)
);

const discordCommands = new Collection(
discordChatInputCommandsById.map(value => [value.name, value])
discordInteractionsById.map(value => [value.name, value])
);

const validCommands = pipe<
Iterable<[string, CommandDataWithHandler]>,
Iterable<string>
>([
const validCommands = pipe([
filter<[string, CommandDataWithHandler]>(
([key, val]: [string, CommandDataWithHandler]) =>
([, val]: [string, CommandDataWithHandler]) =>
'guild' in commandManager && val.guildValidate
? val.guildValidate(commandManager.guild)
: true
),
map(([key]) => key),
map(([key]): string => key),
]);

const newCommands = difference(
Expand Down Expand Up @@ -218,9 +253,7 @@ async function addCommands(
}

function getDestination(
commandManager:
| ApplicationCommandManager
| GuildApplicationCommandManager
commandManager: ApplicationCommandManager | GuildApplicationCommandManager
) {
return 'guild' in commandManager
? `Guild: ${commandManager.guild.name}`
Expand All @@ -236,10 +269,13 @@ function createNewCommands(
const command = cmdDescriptions.get(name);
// this is always true
if (command) {
const { onAttach, handler, ...rest } = command;
const { onAttach, handler, managePermissions, ...rest } = command;
console.info(`Adding Command ${name} for ${destination}`);

return cmdMgr.create(rest);
const guildCmd = await cmdMgr.create(rest);
const { permissions, guild } = guildCmd;

// await managePermissions?.(guild, permissions);
}
});
}
Expand All @@ -250,22 +286,35 @@ function editExistingCommands(
existingCommands: Map<string, ApplicationCommand>
) {
const destination = getDestination(cmdMgr);
return map((name: string) => {
return map(async (name: string) => {
const cmd = cmdDescriptions.get(name);
const existing = existingCommands.get(name);

const { onAttach, handler, ...command } = cmd;

const { onAttach, handler, managePermissions, ...comm } = cmd;
const command = {
defaultPermission: true,
...comm,
}
if (
!isEqual(
getRelevantCmdProperties(cmd),
getRelevantCmdProperties(existing)
)
) {
console.info(`Updating ${name} for ${destination}`);
console.log(
getRelevantCmdProperties(cmd),
getRelevantCmdProperties(existing))

return cmdMgr.edit(existing.id, command);
await cmdMgr.edit(existing.id, command);
}

// try {
// const { permissions, guild } = existing;
// await managePermissions?.(guild, permissions);
// } catch (error) {
// console.log({ error });
// }
});
}

Expand All @@ -278,6 +327,6 @@ function deleteRemovedCommands(
const existing = existingCommands.get(name)!;
console.warn(`Deleting ${name} from ${destination}`);

return cmdMgr.delete(existing.id);
await cmdMgr.delete(existing.id);
});
}
3 changes: 2 additions & 1 deletion src/v2/commands/mdn/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

import type {
ButtonInteraction,
Client,
Expand Down Expand Up @@ -162,6 +162,7 @@ export const mdnCommand: CommandDataWithHandler = {
name: 'mdn',
description: 'search mdn',
handler: async (client, interaction): Promise<void> => {
if(!interaction.isCommand()) { return }
await mdnHandler(client, interaction);
},
options: [
Expand Down
13 changes: 7 additions & 6 deletions src/v2/commands/post/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { cache } from '../../spam_filter/index.js';
import { MultistepForm } from '../../utils/MultistepForm.js';
import { asyncCatch } from '../../utils/asyncCatch.js';
import { createEmbed, createMarkdownCodeBlock } from '../../utils/discordTools.js';
import { lock, unlock } from '../../utils/dmLock';
import { map } from '../../utils/map.js';
import { pipe } from '../../utils/pipe.js';
import { capitalize } from '../../utils/string.js';
Expand Down Expand Up @@ -278,12 +279,12 @@ const handleJobPostingRequest = async (
): Promise<void> => {
const { guild, member } = interaction;
const { user: author } = interaction;
const { username, discriminator, id } = author;
const { username, discriminator, id: userID } = author;

const filter: CollectorFilter<[Message]> = m => m.author.id === id;
const filter: CollectorFilter<[Message]> = m => m.author.id === userID;
const send = (str: string) => author.send(str);
// Generate cache entry
const entry = generateCacheEntry(id);
const entry = generateCacheEntry(userID);

try {
// Check if the user has been cached
Expand Down Expand Up @@ -312,10 +313,10 @@ const handleJobPostingRequest = async (

// Notify the user regarding the rules, and get the channel
const channel = await author.createDM();

const form = new MultistepForm(questions, channel, author);

lock(guild.id, userID, 'JOB_POST_FORM')
const answers = (await form.getResult('guidelines')) as unknown as Answers;
unlock(guild.id, userID, 'JOB_POST_FORM')

console.log(answers)
// Just return if the iteration breaks due to invalid input
Expand All @@ -326,7 +327,7 @@ const handleJobPostingRequest = async (

const url = await createJobPost(answers, guild, {
discriminator,
userID: id,
userID,
username,
});

Expand Down
Loading