Skip to content

Commit 45b5130

Browse files
committed
✨ modmail chat functioality
1 parent 8ffcc6e commit 45b5130

File tree

14 files changed

+5645
-4697
lines changed

14 files changed

+5645
-4697
lines changed

src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const {DUMMY_TOKEN} = process.env;
77
export const {DISCORD_TOKEN} = process.env;
88
export const {REPO_LINK} = process.env;
99

10+
export const {MODMAIL_CHANNEL_ID} = process.env;
1011
export const {MOD_CHANNEL} = process.env;
1112
export const {NUMBER_OF_ALLOWED_MESSAGES} = process.env;
1213
export const {CACHE_REVALIDATION_IN_SECONDS} = process.env;

src/types.d.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
import type {
2+
ApplicationCommandData,
3+
ApplicationCommandPermissionsManager,
24
ChatInputApplicationCommandData,
35
Client,
46
CommandInteraction,
7+
ContextMenuInteraction,
58
Guild,
69
Interaction,
710
} from 'discord.js';
811

9-
export type CommandDataWithHandler = ChatInputApplicationCommandData & {
10-
handler: (client: Client, interaction: CommandInteraction) => Promise<void>;
12+
export type CommandDataWithHandler = ApplicationCommandData & {
13+
handler: (
14+
client: Client,
15+
interaction: CommandInteraction | ContextMenuInteraction
16+
) => Promise<void>;
1117
onAttach?: (client: Client) => void;
1218
guildValidate?: (guild: Guild) => boolean;
19+
managePermissions?: (
20+
guild: Guild,
21+
permissions: ApplicationCommandPermissionsManager<
22+
{
23+
guild: GuildResolvable;
24+
},
25+
{
26+
guild: GuildResolvable;
27+
},
28+
{
29+
guild: GuildResolvable;
30+
},
31+
Guild,
32+
string
33+
>
34+
) => Promise<void>;
1335
};

src/v2/commands/index.ts

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// we need to await in a loop as we're rate-limited anyway
2+
/* eslint-disable no-await-in-loop */
13
/* eslint-disable @typescript-eslint/no-non-null-assertion */
24
import type {
35
ApplicationCommand,
@@ -7,12 +9,15 @@ import type {
79
Guild,
810
GuildApplicationCommandManager,
911
GuildResolvable,
12+
ApplicationCommandType,
1013
} from 'discord.js';
1114
import { Collection } from 'discord.js';
15+
import { ApplicationCommandTypes } from 'discord.js/typings/enums';
1216
import { filter } from 'domyno';
1317
import { isEqual } from 'lodash-es';
1418

1519
import type { CommandDataWithHandler } from '../../types';
20+
import { commands } from '../modules/modmail';
1621
import { asyncCatch } from '../utils/asyncCatch.js';
1722
import { map, mapʹ } from '../utils/map.js';
1823
import { merge } from '../utils/merge.js';
@@ -46,6 +51,7 @@ export const guildCommands = new Map(
4651
shitpostInteraction,
4752
npmInteraction,
4853
whynoInteraction,
54+
...commands,
4955
// warn // Not used atm
5056
].map(command => [command.name, command])
5157
); // placeholder for now
@@ -58,16 +64,22 @@ export const applicationCommands = new Collection<
5864
const getRelevantCmdProperties = ({
5965
description,
6066
name,
67+
type = ApplicationCommandTypes.CHAT_INPUT,
6168
options,
69+
defaultPermission = true,
6270
}: {
63-
description: string;
71+
type?: ApplicationCommandTypes | ApplicationCommandType;
72+
description?: string;
6473
name: string;
6574
options?: unknown[];
75+
defaultPermission?: boolean;
6676
}): ApplicationCommandData => {
6777
const relevantData = {
78+
type: _normalizeType(type),
6879
description,
6980
name,
7081
options,
82+
defaultPermission,
7183
} as unknown as ApplicationCommandData;
7284
return stripNullish(normalizeApplicationCommandData(relevantData));
7385
};
@@ -92,7 +104,7 @@ export const registerCommands = async (client: Client): Promise<void> => {
92104
client.on(
93105
'interactionCreate',
94106
asyncCatch(async interaction => {
95-
if (!interaction.isCommand()) {
107+
if (!interaction.isCommand() && !interaction.isContextMenu()) {
96108
return;
97109
}
98110

@@ -123,13 +135,13 @@ export const registerCommands = async (client: Client): Promise<void> => {
123135

124136
for (const { onAttach } of applicationCommands.values()) {
125137
// We're attaching these so it's fine
126-
138+
127139
onAttach?.(client);
128140
}
129141

130142
for (const { onAttach } of guildCommands.values()) {
131143
// We're attaching these so it's fine
132-
144+
133145
onAttach?.(client);
134146
}
135147

@@ -161,6 +173,25 @@ export const registerCommands = async (client: Client): Promise<void> => {
161173
// })
162174
};
163175

176+
function _normalizeType(
177+
type: ApplicationCommandType | ApplicationCommandTypes
178+
) {
179+
if (typeof type === 'number') {
180+
return type;
181+
}
182+
183+
switch (type) {
184+
case 'MESSAGE':
185+
return ApplicationCommandTypes.MESSAGE;
186+
case 'USER':
187+
return ApplicationCommandTypes.USER;
188+
case 'CHAT_INPUT':
189+
default:
190+
return ApplicationCommandTypes.CHAT_INPUT;
191+
}
192+
}
193+
194+
const interactionTypes = new Set(['CHAT_INPUT','USER','MESSAGE'])
164195
async function addCommands(
165196
serverCommands: Collection<
166197
string,
@@ -169,25 +200,22 @@ async function addCommands(
169200
commandDescriptions: Map<string, CommandDataWithHandler>,
170201
commandManager: ApplicationCommandManager | GuildApplicationCommandManager
171202
) {
172-
const discordChatInputCommandsById = serverCommands.filter(
173-
x => x.type === 'CHAT_INPUT'
203+
const discordInteractionsById = serverCommands.filter(
204+
x => interactionTypes.has(x.type)
174205
);
175206

176207
const discordCommands = new Collection(
177-
discordChatInputCommandsById.map(value => [value.name, value])
208+
discordInteractionsById.map(value => [value.name, value])
178209
);
179210

180-
const validCommands = pipe<
181-
Iterable<[string, CommandDataWithHandler]>,
182-
Iterable<string>
183-
>([
211+
const validCommands = pipe([
184212
filter<[string, CommandDataWithHandler]>(
185-
([key, val]: [string, CommandDataWithHandler]) =>
213+
([, val]: [string, CommandDataWithHandler]) =>
186214
'guild' in commandManager && val.guildValidate
187215
? val.guildValidate(commandManager.guild)
188216
: true
189217
),
190-
map(([key]) => key),
218+
map(([key]): string => key),
191219
]);
192220

193221
const newCommands = difference(
@@ -218,9 +246,7 @@ async function addCommands(
218246
}
219247

220248
function getDestination(
221-
commandManager:
222-
| ApplicationCommandManager
223-
| GuildApplicationCommandManager
249+
commandManager: ApplicationCommandManager | GuildApplicationCommandManager
224250
) {
225251
return 'guild' in commandManager
226252
? `Guild: ${commandManager.guild.name}`
@@ -236,10 +262,13 @@ function createNewCommands(
236262
const command = cmdDescriptions.get(name);
237263
// this is always true
238264
if (command) {
239-
const { onAttach, handler, ...rest } = command;
265+
const { onAttach, handler, managePermissions, ...rest } = command;
240266
console.info(`Adding Command ${name} for ${destination}`);
241267

242-
return cmdMgr.create(rest);
268+
const guildCmd = await cmdMgr.create(rest);
269+
const { permissions, guild } = guildCmd;
270+
271+
await managePermissions?.(guild, permissions);
243272
}
244273
});
245274
}
@@ -250,11 +279,11 @@ function editExistingCommands(
250279
existingCommands: Map<string, ApplicationCommand>
251280
) {
252281
const destination = getDestination(cmdMgr);
253-
return map((name: string) => {
282+
return map(async (name: string) => {
254283
const cmd = cmdDescriptions.get(name);
255284
const existing = existingCommands.get(name);
256285

257-
const { onAttach, handler, ...command } = cmd;
286+
const { onAttach, handler, managePermissions, ...command } = cmd;
258287

259288
if (
260289
!isEqual(
@@ -263,8 +292,18 @@ function editExistingCommands(
263292
)
264293
) {
265294
console.info(`Updating ${name} for ${destination}`);
295+
console.log(
296+
getRelevantCmdProperties(cmd),
297+
getRelevantCmdProperties(existing))
266298

267-
return cmdMgr.edit(existing.id, command);
299+
await cmdMgr.edit(existing.id, command);
300+
}
301+
302+
try {
303+
const { permissions, guild } = existing;
304+
await managePermissions?.(guild, permissions);
305+
} catch (error) {
306+
console.log({ error });
268307
}
269308
});
270309
}
@@ -278,6 +317,6 @@ function deleteRemovedCommands(
278317
const existing = existingCommands.get(name)!;
279318
console.warn(`Deleting ${name} from ${destination}`);
280319

281-
return cmdMgr.delete(existing.id);
320+
await cmdMgr.delete(existing.id);
282321
});
283322
}

src/v2/commands/mdn/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
import type {
33
ButtonInteraction,
44
Client,
@@ -162,6 +162,7 @@ export const mdnCommand: CommandDataWithHandler = {
162162
name: 'mdn',
163163
description: 'search mdn',
164164
handler: async (client, interaction): Promise<void> => {
165+
if(!interaction.isCommand()) { return }
165166
await mdnHandler(client, interaction);
166167
},
167168
options: [

src/v2/commands/shitpost/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,21 @@ export const shitpostInteraction: CommandDataWithHandler = {
4545
description:
4646
'A fun little shitpost command using some of the about/please/whyno commands',
4747
guildValidate: guild => guild.id === SERVER_ID,
48+
defaultPermission: false,
49+
async managePermissions(guild, permissions){
50+
await permissions.add({
51+
guild,
52+
permissions: [
53+
{
54+
id: HELPFUL_ROLE_ID,
55+
permission: true,
56+
type: "ROLE"
57+
}
58+
]
59+
})
60+
},
4861
handler: async (client, interaction) => {
62+
if(!interaction.isCommand()) { return }
4963
const topic = interaction.options.getString('topic');
5064
const replacement = interaction.options.getString('replacement');
5165
const content = aboutMessages.get(topic);

src/v2/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { limitFnByUser } from './cache/index.js';
3030
import { registerCommands } from './commands/index.js';
3131
import pointDecaySystem, { loadLastDecayFromDB } from './helpful_role/point_decay.js';
3232
import { registerMessageContextMenu } from './message_context/index.js';
33+
import { handleDM } from './modules/modmail/index.js';
3334
import { registerUserContextMenu } from './user_context/index.js';
3435
import { asyncCatch } from './utils/asyncCatch.js';
3536
import { stripMarkdownQuote } from './utils/content_format.js';
@@ -68,6 +69,7 @@ const client = new Client({
6869
Intents.FLAGS.DIRECT_MESSAGE_REACTIONS,
6970
Intents.FLAGS.DIRECT_MESSAGE_TYPING,
7071
],
72+
partials: ['CHANNEL']
7173
});
7274

7375
const blacklistedServer = new Set([
@@ -138,10 +140,14 @@ const detectDeprecatedCommands = limitFnByUser(handleDeprecatedCommands, {
138140
const isWebdevAndWebDesignServer = (msg: Message) =>
139141
msg.guild?.id === SERVER_ID || false;
140142

143+
141144
client.on('messageCreate', msg => {
142145
if (msg.author.bot) {
143146
return;
144147
}
148+
if(msg.channel.type === 'DM') {
149+
return handleDM(msg)
150+
}
145151

146152
if (NON_COMMAND_MSG_TYPES.has(msg.channel.type) && msg.guild) {
147153
handleNonCommandGuildMessages(msg);

src/v2/message_context/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
2+
33
import type { Client } from 'discord.js';
44
import { Collection } from 'discord.js';
55

@@ -15,8 +15,8 @@ export const registerMessageContextMenu = async (client: Client): Promise<void>
1515

1616
client.application.commands.set([])
1717

18-
client.on("interactionCreate", interaction => {
19-
if(!interaction.isContextMenu() || interaction.targetType !== "MESSAGE") {return}
20-
console.log(interaction)
21-
})
18+
// client.on("interactionCreate", interaction => {
19+
// if(!interaction.isContextMenu() || interaction.targetType !== "MESSAGE") {return}
20+
// // console.log(interaction)
21+
// })
2222
}

0 commit comments

Comments
 (0)