add /pick-role and /remove-role, refactor responses

This commit is contained in:
41666 2022-02-04 21:21:32 -05:00
parent 0836d548b2
commit 5aa5a6ae1c
7 changed files with 253 additions and 54 deletions

View file

@ -0,0 +1,18 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import { rolePickerCommon } from '@roleypoly/api/src/routes/interactions/role-picker-common';
import { Context } from '@roleypoly/api/src/utils/context';
import {
InteractionRequest,
InteractionResponse,
TransactionType,
} from '@roleypoly/types';
export const pickRole: InteractionHandler = async (
interaction: InteractionRequest,
context: Context
): Promise<InteractionResponse> => {
return rolePickerCommon(interaction, context, TransactionType.Add);
};
pickRole.ephemeral = true;
pickRole.deferred = true;

View file

@ -9,6 +9,10 @@ import {
getName,
InteractionHandler,
} from '@roleypoly/api/src/routes/interactions/helpers';
import {
embedPalette,
embedResponse,
} from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import {
CategoryType,
@ -24,20 +28,13 @@ export const pickableRoles: InteractionHandler = async (
context: Context
): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: 0xff0000,
title: ':x: Error',
description: `:x: Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`,
},
],
},
};
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`,
embedPalette.error
);
}
const [guildData, guild, member] = await Promise.all([
@ -47,41 +44,26 @@ export const pickableRoles: InteractionHandler = async (
]);
if (!guildData || !guild) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: 0xff0000,
title: ':x: Error',
description: `:x: Hey ${getName(
interaction
)}. Something's wrong with the server you're in. Try picking your roles at ${
context.config.uiPublicURI
}/s/${interaction.guild_id} instead.`,
},
],
},
};
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. Something's wrong with the server you're in. Try picking your roles at ${
context.config.uiPublicURI
}/s/${interaction.guild_id} instead.`,
embedPalette.error
);
}
const roles = getPickableRoles(guildData, guild);
if (roles.length === 0) {
console.warn('/pickable-roles turned up empty?', { roles, guild, guildData });
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: 0xff0000,
title: ':fire: Error',
description: `Hey ${getName(
interaction
)}. This server might not be set up to use Roleypoly yet, as there are no roles to pick from.`,
},
],
},
};
return embedResponse(
':fire: Error',
`Hey ${getName(
interaction
)}. This server might not be set up to use Roleypoly yet, as there are no roles to pick from.`,
embedPalette.error
);
}
const makeBoldIfMemberHasRole = (role: Role, base: string): string => {
@ -93,7 +75,7 @@ export const pickableRoles: InteractionHandler = async (
};
const embed: Embed = {
color: 0xab9b9a,
color: embedPalette.neutral,
fields: roles.map(({ category, roles }) => {
return {
name: `${category.name}${

View file

@ -0,0 +1,18 @@
import { InteractionHandler } from '@roleypoly/api/src/routes/interactions/helpers';
import { rolePickerCommon } from '@roleypoly/api/src/routes/interactions/role-picker-common';
import { Context } from '@roleypoly/api/src/utils/context';
import {
InteractionRequest,
InteractionResponse,
TransactionType,
} from '@roleypoly/types';
export const removeRole: InteractionHandler = async (
interaction: InteractionRequest,
context: Context
): Promise<InteractionResponse> => {
return rolePickerCommon(interaction, context, TransactionType.Remove);
};
removeRole.ephemeral = true;
removeRole.deferred = true;

View file

@ -2,6 +2,7 @@ import {
getName,
InteractionHandler,
} from '@roleypoly/api/src/routes/interactions/helpers';
import { embedResponse } from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import {
Embed,
@ -15,14 +16,12 @@ export const roleypoly: InteractionHandler = (
context: Context
): InteractionResponse => {
if (!interaction.guild_id) {
return {
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:x: Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`,
},
};
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`
);
}
return {

View file

@ -1,5 +1,7 @@
import { helloWorld } from '@roleypoly/api/src/routes/interactions/commands/hello-world';
import { pickRole } from '@roleypoly/api/src/routes/interactions/commands/pick-role';
import { pickableRoles } from '@roleypoly/api/src/routes/interactions/commands/pickable-roles';
import { removeRole } from '@roleypoly/api/src/routes/interactions/commands/remove-role';
import { roleypoly } from '@roleypoly/api/src/routes/interactions/commands/roleypoly';
import {
InteractionHandler,
@ -22,6 +24,8 @@ const commands: Record<InteractionData['name'], InteractionHandler> = {
'hello-world': helloWorld,
roleypoly: roleypoly,
'pickable-roles': pickableRoles,
'pick-role': pickRole,
'remove-role': removeRole,
};
export const handleInteraction: RoleypolyHandler = async (

View file

@ -36,3 +36,26 @@ export const notImplemented: InteractionHandler = (): InteractionResponse => ({
flags: InteractionFlags.EPHEMERAL,
},
});
export const embedResponse = (
title: string,
description: string,
color?: number
): InteractionResponse => ({
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
color: color || 0x00ff00,
title,
description,
},
],
},
});
export const embedPalette = {
success: 0x1d8227,
error: 0xf14343,
neutral: 0x2c2f33,
};

View file

@ -0,0 +1,155 @@
import { getGuild, getGuildData } from '@roleypoly/api/src/guilds/getters';
import { calculateNewRoles } from '@roleypoly/api/src/routes/guilds/guild-roles-put';
import { getName } from '@roleypoly/api/src/routes/interactions/helpers';
import {
embedPalette,
embedResponse,
} from '@roleypoly/api/src/routes/interactions/responses';
import { Context } from '@roleypoly/api/src/utils/context';
import { APIMember, AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
import { isIdenticalArray } from '@roleypoly/misc-utils/collection-tools';
import {
CategoryType,
InteractionRequest,
InteractionResponse,
RoleTransaction,
TransactionType,
} from '@roleypoly/types';
export const rolePickerCommon = async (
interaction: InteractionRequest,
context: Context,
action: TransactionType
): Promise<InteractionResponse> => {
if (!interaction.guild_id) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. You need to use this command in a server, not in a DM.`
);
}
const currentRoles = interaction.member?.roles || [];
const [guildData, guild] = await Promise.all([
getGuildData(context.config, interaction.guild_id),
getGuild(context.config, interaction.guild_id),
]);
if (!guildData || !guild) {
return embedResponse(
':x: Error',
`Hey ${getName(
interaction
)}. Something's wrong with the server you're in. Try picking your roles at ${
context.config.uiPublicURI
}/s/${interaction.guild_id} instead.`,
embedPalette.error
);
}
const roleToPick = interaction.data?.options?.[0]?.value;
if (!roleToPick) {
return embedResponse(
':fire: Discord sent me the wrong data',
`Hey ${getName(interaction)}. Please try again later.`,
embedPalette.error
);
}
const hasRole = interaction.member?.roles.includes(roleToPick);
if (action === TransactionType.Add && hasRole) {
return embedResponse(
`:white_check_mark: You already have that role.`,
`Hey ${getName(interaction)}. You already have <@&${roleToPick}>!`,
embedPalette.neutral
);
}
if (action === TransactionType.Remove && !hasRole) {
return embedResponse(
`:white_check_mark: You don't have that role.`,
`Hey ${getName(interaction)}. You already don't have <@&${roleToPick}>!`,
embedPalette.neutral
);
}
const extraTransactions: RoleTransaction[] = [];
let isSingle = false;
if (action === TransactionType.Add) {
// For single-type categories, let's also generate the remove rules for the other roles in the category
const category = guildData.categories.find((category) =>
category.roles.includes(roleToPick)
);
if (category?.type === CategoryType.Single) {
const otherRoles = category.roles.filter((role) => role !== roleToPick);
extraTransactions.push(
...otherRoles.map((role) => ({ action: TransactionType.Remove, id: role }))
);
isSingle = true;
}
}
const newRoles = calculateNewRoles({
currentRoles,
guildRoles: guild.roles,
guildData,
updateRequest: {
knownState: currentRoles,
transactions: [{ action, id: roleToPick }, ...extraTransactions],
},
});
if (isIdenticalArray(currentRoles, newRoles)) {
return embedResponse(
':x: You cannot pick this role.',
`Hey ${getName(
interaction
)}. <@&${roleToPick}> isn't pickable. Check /pickable-roles to see which roles you can use.`,
embedPalette.error
);
}
const patchMemberRoles = await discordFetch<APIMember>(
`/guilds/${interaction.guild_id}/members/${interaction.member?.user?.id}`,
context.config.botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
'x-audit-log-reason': `Picked their roles via /${
action === TransactionType.Add ? 'pick' : 'remove'
}-role`,
},
body: JSON.stringify({
roles: newRoles,
}),
}
);
if (!patchMemberRoles) {
return embedResponse(
':x: Discord stopped me from updating your roles.',
`Hey ${getName(
interaction
)}. Discord didn't let me give you <@&${roleToPick}>. Could you try again later?`,
embedPalette.error
);
}
return action === TransactionType.Add
? embedResponse(
':white_check_mark: You got it!',
`Hey ${getName(interaction)}, I gave you <@&${roleToPick}>!${
isSingle ? `\nThe other roles in this category have been removed.` : ''
}`,
embedPalette.success
)
: embedResponse(
":white_check_mark: You (don't) got it!",
`Hey ${getName(interaction)}, I took away <@&${roleToPick}>!`,
embedPalette.success
);
};