mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
add /pick-role and /remove-role, refactor responses
This commit is contained in:
parent
0836d548b2
commit
5aa5a6ae1c
7 changed files with 253 additions and 54 deletions
18
packages/api/src/routes/interactions/commands/pick-role.ts
Normal file
18
packages/api/src/routes/interactions/commands/pick-role.ts
Normal 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;
|
|
@ -9,6 +9,10 @@ import {
|
||||||
getName,
|
getName,
|
||||||
InteractionHandler,
|
InteractionHandler,
|
||||||
} from '@roleypoly/api/src/routes/interactions/helpers';
|
} 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 { Context } from '@roleypoly/api/src/utils/context';
|
||||||
import {
|
import {
|
||||||
CategoryType,
|
CategoryType,
|
||||||
|
@ -24,20 +28,13 @@ export const pickableRoles: InteractionHandler = async (
|
||||||
context: Context
|
context: Context
|
||||||
): Promise<InteractionResponse> => {
|
): Promise<InteractionResponse> => {
|
||||||
if (!interaction.guild_id) {
|
if (!interaction.guild_id) {
|
||||||
return {
|
return embedResponse(
|
||||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
':x: Error',
|
||||||
data: {
|
`Hey ${getName(
|
||||||
embeds: [
|
interaction
|
||||||
{
|
)}. You need to use this command in a server, not in a DM.`,
|
||||||
color: 0xff0000,
|
embedPalette.error
|
||||||
title: ':x: Error',
|
);
|
||||||
description: `:x: Hey ${getName(
|
|
||||||
interaction
|
|
||||||
)}. You need to use this command in a server, not in a DM.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [guildData, guild, member] = await Promise.all([
|
const [guildData, guild, member] = await Promise.all([
|
||||||
|
@ -47,41 +44,26 @@ export const pickableRoles: InteractionHandler = async (
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!guildData || !guild) {
|
if (!guildData || !guild) {
|
||||||
return {
|
return embedResponse(
|
||||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
':x: Error',
|
||||||
data: {
|
`Hey ${getName(
|
||||||
embeds: [
|
interaction
|
||||||
{
|
)}. Something's wrong with the server you're in. Try picking your roles at ${
|
||||||
color: 0xff0000,
|
context.config.uiPublicURI
|
||||||
title: ':x: Error',
|
}/s/${interaction.guild_id} instead.`,
|
||||||
description: `:x: Hey ${getName(
|
embedPalette.error
|
||||||
interaction
|
);
|
||||||
)}. Something's wrong with the server you're in. Try picking your roles at ${
|
|
||||||
context.config.uiPublicURI
|
|
||||||
}/s/${interaction.guild_id} instead.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = getPickableRoles(guildData, guild);
|
const roles = getPickableRoles(guildData, guild);
|
||||||
if (roles.length === 0) {
|
if (roles.length === 0) {
|
||||||
console.warn('/pickable-roles turned up empty?', { roles, guild, guildData });
|
return embedResponse(
|
||||||
return {
|
':fire: Error',
|
||||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
`Hey ${getName(
|
||||||
data: {
|
interaction
|
||||||
embeds: [
|
)}. This server might not be set up to use Roleypoly yet, as there are no roles to pick from.`,
|
||||||
{
|
embedPalette.error
|
||||||
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.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeBoldIfMemberHasRole = (role: Role, base: string): string => {
|
const makeBoldIfMemberHasRole = (role: Role, base: string): string => {
|
||||||
|
@ -93,7 +75,7 @@ export const pickableRoles: InteractionHandler = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const embed: Embed = {
|
const embed: Embed = {
|
||||||
color: 0xab9b9a,
|
color: embedPalette.neutral,
|
||||||
fields: roles.map(({ category, roles }) => {
|
fields: roles.map(({ category, roles }) => {
|
||||||
return {
|
return {
|
||||||
name: `${category.name}${
|
name: `${category.name}${
|
||||||
|
|
18
packages/api/src/routes/interactions/commands/remove-role.ts
Normal file
18
packages/api/src/routes/interactions/commands/remove-role.ts
Normal 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;
|
|
@ -2,6 +2,7 @@ import {
|
||||||
getName,
|
getName,
|
||||||
InteractionHandler,
|
InteractionHandler,
|
||||||
} from '@roleypoly/api/src/routes/interactions/helpers';
|
} 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 { Context } from '@roleypoly/api/src/utils/context';
|
||||||
import {
|
import {
|
||||||
Embed,
|
Embed,
|
||||||
|
@ -15,14 +16,12 @@ export const roleypoly: InteractionHandler = (
|
||||||
context: Context
|
context: Context
|
||||||
): InteractionResponse => {
|
): InteractionResponse => {
|
||||||
if (!interaction.guild_id) {
|
if (!interaction.guild_id) {
|
||||||
return {
|
return embedResponse(
|
||||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
':x: Error',
|
||||||
data: {
|
`Hey ${getName(
|
||||||
content: `:x: Hey ${getName(
|
interaction
|
||||||
interaction
|
)}. You need to use this command in a server, not in a DM.`
|
||||||
)}. You need to use this command in a server, not in a DM.`,
|
);
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { helloWorld } from '@roleypoly/api/src/routes/interactions/commands/hello-world';
|
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 { 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 { roleypoly } from '@roleypoly/api/src/routes/interactions/commands/roleypoly';
|
||||||
import {
|
import {
|
||||||
InteractionHandler,
|
InteractionHandler,
|
||||||
|
@ -22,6 +24,8 @@ const commands: Record<InteractionData['name'], InteractionHandler> = {
|
||||||
'hello-world': helloWorld,
|
'hello-world': helloWorld,
|
||||||
roleypoly: roleypoly,
|
roleypoly: roleypoly,
|
||||||
'pickable-roles': pickableRoles,
|
'pickable-roles': pickableRoles,
|
||||||
|
'pick-role': pickRole,
|
||||||
|
'remove-role': removeRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleInteraction: RoleypolyHandler = async (
|
export const handleInteraction: RoleypolyHandler = async (
|
||||||
|
|
|
@ -36,3 +36,26 @@ export const notImplemented: InteractionHandler = (): InteractionResponse => ({
|
||||||
flags: InteractionFlags.EPHEMERAL,
|
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,
|
||||||
|
};
|
||||||
|
|
155
packages/api/src/routes/interactions/role-picker-common.ts
Normal file
155
packages/api/src/routes/interactions/role-picker-common.ts
Normal 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
|
||||||
|
);
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue