mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
add /roleypoly and /pickable-roles slash commands, fix framework issues
This commit is contained in:
parent
fd7ed13e9d
commit
5c5258ef5e
6 changed files with 329 additions and 19 deletions
|
@ -1,7 +1,7 @@
|
|||
jest.mock('../utils/discord');
|
||||
jest.mock('../utils/legacy');
|
||||
|
||||
import { CategoryType, Features, GuildData } from '@roleypoly/types';
|
||||
import { CategoryType, Features, Guild, GuildData, RoleSafety } from '@roleypoly/types';
|
||||
import { APIGuild, discordFetch } from '../utils/discord';
|
||||
import {
|
||||
fetchLegacyServer,
|
||||
|
@ -9,7 +9,7 @@ import {
|
|||
transformLegacyGuild,
|
||||
} from '../utils/legacy';
|
||||
import { configContext } from '../utils/testHelpers';
|
||||
import { getGuild, getGuildData, getGuildMember } from './getters';
|
||||
import { getGuild, getGuildData, getGuildMember, getPickableRoles } from './getters';
|
||||
|
||||
const mockDiscordFetch = discordFetch as jest.Mock;
|
||||
const mockFetchLegacyServer = fetchLegacyServer as jest.Mock;
|
||||
|
@ -241,3 +241,81 @@ describe('getGuildMember', () => {
|
|||
expect(result!.nick).toBe('test2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPickableRoles', () => {
|
||||
it('returns all pickable roles for a given guild', async () => {
|
||||
const guildData: GuildData = {
|
||||
id: '123',
|
||||
message: 'Hello world!',
|
||||
categories: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'test',
|
||||
position: 0,
|
||||
roles: ['role-1', 'role-2', 'role-unsafe'],
|
||||
hidden: false,
|
||||
type: CategoryType.Multi,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
name: 'test',
|
||||
position: 0,
|
||||
roles: ['role-3', 'role-4'],
|
||||
hidden: true,
|
||||
type: CategoryType.Multi,
|
||||
},
|
||||
],
|
||||
features: Features.None,
|
||||
auditLogWebhook: null,
|
||||
accessControl: {
|
||||
allowList: [],
|
||||
blockList: [],
|
||||
blockPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
const guild: Guild = {
|
||||
id: '123',
|
||||
name: 'test',
|
||||
icon: '',
|
||||
roles: [
|
||||
{
|
||||
id: 'role-1',
|
||||
name: 'test',
|
||||
position: 0,
|
||||
managed: false,
|
||||
color: 0,
|
||||
safety: RoleSafety.Safe,
|
||||
permissions: '0',
|
||||
},
|
||||
{
|
||||
id: 'role-3',
|
||||
name: 'test',
|
||||
position: 0,
|
||||
managed: false,
|
||||
color: 0,
|
||||
safety: RoleSafety.Safe,
|
||||
permissions: '0',
|
||||
},
|
||||
{
|
||||
id: 'role-unsafe',
|
||||
name: 'test',
|
||||
position: 0,
|
||||
managed: false,
|
||||
color: 0,
|
||||
safety: RoleSafety.DangerousPermissions,
|
||||
permissions: '0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getPickableRoles(guildData, guild);
|
||||
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
category: guildData.categories[0],
|
||||
roles: [guild.roles[0]],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { fetchLegacyServer, transformLegacyGuild } from '@roleypoly/api/src/utils/legacy';
|
||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||
import {
|
||||
Category,
|
||||
Features,
|
||||
Guild,
|
||||
GuildData,
|
||||
|
@ -199,3 +200,31 @@ const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: numbe
|
|||
|
||||
return safety;
|
||||
};
|
||||
|
||||
export const getPickableRoles = (
|
||||
guildData: GuildData,
|
||||
guild: Guild
|
||||
): { category: Category; roles: Role[] }[] => {
|
||||
const pickableRoles: { category: Category; roles: Role[] }[] = [];
|
||||
|
||||
for (const category of guildData.categories) {
|
||||
if (category.roles.length === 0 || category.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const roles = category.roles
|
||||
.map((roleID) => guild.roles.find((r) => r.id === roleID))
|
||||
.filter((role) => role !== undefined && role.safety === RoleSafety.Safe) as Role[];
|
||||
|
||||
if (roles.length > 0) {
|
||||
pickableRoles.push({
|
||||
category,
|
||||
roles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log({ pickableRoles });
|
||||
|
||||
return pickableRoles;
|
||||
};
|
||||
|
|
135
packages/api/src/routes/interactions/commands/pickable-roles.ts
Normal file
135
packages/api/src/routes/interactions/commands/pickable-roles.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import {
|
||||
getGuild,
|
||||
getGuildData,
|
||||
getGuildMember,
|
||||
getPickableRoles,
|
||||
} from '@roleypoly/api/src/guilds/getters';
|
||||
import {
|
||||
getName,
|
||||
InteractionHandler,
|
||||
} from '@roleypoly/api/src/routes/interactions/helpers';
|
||||
import { Context } from '@roleypoly/api/src/utils/context';
|
||||
import {
|
||||
CategoryType,
|
||||
Embed,
|
||||
InteractionCallbackType,
|
||||
InteractionRequest,
|
||||
InteractionResponse,
|
||||
Role,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const pickableRoles: InteractionHandler = async (
|
||||
interaction: InteractionRequest,
|
||||
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.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [guildData, guild, member] = await Promise.all([
|
||||
getGuildData(context.config, interaction.guild_id),
|
||||
getGuild(context.config, interaction.guild_id),
|
||||
getGuildMember(context.config, interaction.guild_id, interaction.member?.user?.id!),
|
||||
]);
|
||||
|
||||
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.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const makeBoldIfMemberHasRole = (role: Role, base: string): string => {
|
||||
if (member?.roles.includes(role.id)) {
|
||||
return `__${base}__`;
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
const embed: Embed = {
|
||||
color: 0xab9b9a,
|
||||
fields: roles.map(({ category, roles }) => {
|
||||
return {
|
||||
name: `${category.name}${
|
||||
category.type === CategoryType.Single ? ' *(pick one)*' : ''
|
||||
}`,
|
||||
value: roles
|
||||
.map((role) => makeBoldIfMemberHasRole(role, `<@&${role.id}>`))
|
||||
.join(', '),
|
||||
} as Embed['fields'][0];
|
||||
}),
|
||||
title: 'You can pick any of these roles with /pick-role',
|
||||
footer: {
|
||||
text: `Roles with an __underline__ are already picked by you.`,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [embed],
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
// Link to Roleypoly
|
||||
{
|
||||
type: 2,
|
||||
label: 'Pick roles on your browser',
|
||||
url: `${context.config.uiPublicURI}/s/${interaction.guild_id}`,
|
||||
style: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
pickableRoles.ephemeral = true;
|
||||
pickableRoles.deferred = true;
|
61
packages/api/src/routes/interactions/commands/roleypoly.ts
Normal file
61
packages/api/src/routes/interactions/commands/roleypoly.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
getName,
|
||||
InteractionHandler,
|
||||
} from '@roleypoly/api/src/routes/interactions/helpers';
|
||||
import { Context } from '@roleypoly/api/src/utils/context';
|
||||
import {
|
||||
Embed,
|
||||
InteractionCallbackType,
|
||||
InteractionRequest,
|
||||
InteractionResponse,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const roleypoly: InteractionHandler = (
|
||||
interaction: InteractionRequest,
|
||||
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 {
|
||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
color: 0x453e3d,
|
||||
title: `:beginner: Hey there, ${getName(interaction)}!`,
|
||||
description: `Try these slash commands, or pick roles from your browser!`,
|
||||
fields: [
|
||||
{ name: 'See all the roles', value: '/pickable-roles' },
|
||||
{ name: 'Pick a role', value: '/pick-role' },
|
||||
{ name: 'Remove a role', value: '/remove-role' },
|
||||
],
|
||||
} as Embed,
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
// Link to Roleypoly
|
||||
{
|
||||
type: 2,
|
||||
label: `Pick roles on ${new URL(context.config.uiPublicURI).hostname}`,
|
||||
url: `${context.config.uiPublicURI}/s/${interaction.guild_id}`,
|
||||
style: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
roleypoly.ephemeral = true;
|
|
@ -1,12 +1,7 @@
|
|||
import { Config } from '@roleypoly/api/src/utils/config';
|
||||
import { Context } from '@roleypoly/api/src/utils/context';
|
||||
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
|
||||
import {
|
||||
InteractionCallbackType,
|
||||
InteractionFlags,
|
||||
InteractionRequest,
|
||||
InteractionResponse,
|
||||
} from '@roleypoly/types';
|
||||
import { InteractionRequest, InteractionResponse } from '@roleypoly/types';
|
||||
|
||||
export const verifyRequest = async (
|
||||
config: Config,
|
||||
|
@ -76,17 +71,19 @@ export const runAsync = async (
|
|||
|
||||
try {
|
||||
const response = await handler(interaction, context);
|
||||
if (!response) {
|
||||
throw new Error('Interaction handler returned no response');
|
||||
}
|
||||
|
||||
console.log({ response });
|
||||
|
||||
await discordFetch(url, '', AuthType.None, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
||||
...response.data,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -105,13 +102,18 @@ export const runAsync = async (
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "I'm sorry, I'm having trouble processing this request.",
|
||||
flags: InteractionFlags.EPHEMERAL,
|
||||
},
|
||||
} as InteractionResponse),
|
||||
} as InteractionResponse['data']),
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
export const getName = (interaction: InteractionRequest): string => {
|
||||
return (
|
||||
interaction.member?.nick ||
|
||||
interaction.member?.user?.username ||
|
||||
interaction.user?.username ||
|
||||
'friend'
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { helloWorld } from '@roleypoly/api/src/routes/interactions/commands/hello-world';
|
||||
import { pickableRoles } from '@roleypoly/api/src/routes/interactions/commands/pickable-roles';
|
||||
import { roleypoly } from '@roleypoly/api/src/routes/interactions/commands/roleypoly';
|
||||
import {
|
||||
InteractionHandler,
|
||||
runAsync,
|
||||
|
@ -18,6 +20,8 @@ import {
|
|||
|
||||
const commands: Record<InteractionData['name'], InteractionHandler> = {
|
||||
'hello-world': helloWorld,
|
||||
roleypoly: roleypoly,
|
||||
'pickable-roles': pickableRoles,
|
||||
};
|
||||
|
||||
export const handleInteraction: RoleypolyHandler = async (
|
||||
|
@ -57,6 +61,7 @@ export const handleInteraction: RoleypolyHandler = async (
|
|||
return json({
|
||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Figuring it out...',
|
||||
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
||||
},
|
||||
} as InteractionResponse);
|
||||
|
|
Loading…
Add table
Reference in a new issue