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/discord');
|
||||||
jest.mock('../utils/legacy');
|
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 { APIGuild, discordFetch } from '../utils/discord';
|
||||||
import {
|
import {
|
||||||
fetchLegacyServer,
|
fetchLegacyServer,
|
||||||
|
@ -9,7 +9,7 @@ import {
|
||||||
transformLegacyGuild,
|
transformLegacyGuild,
|
||||||
} from '../utils/legacy';
|
} from '../utils/legacy';
|
||||||
import { configContext } from '../utils/testHelpers';
|
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 mockDiscordFetch = discordFetch as jest.Mock;
|
||||||
const mockFetchLegacyServer = fetchLegacyServer as jest.Mock;
|
const mockFetchLegacyServer = fetchLegacyServer as jest.Mock;
|
||||||
|
@ -241,3 +241,81 @@ describe('getGuildMember', () => {
|
||||||
expect(result!.nick).toBe('test2');
|
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 { fetchLegacyServer, transformLegacyGuild } from '@roleypoly/api/src/utils/legacy';
|
||||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||||
import {
|
import {
|
||||||
|
Category,
|
||||||
Features,
|
Features,
|
||||||
Guild,
|
Guild,
|
||||||
GuildData,
|
GuildData,
|
||||||
|
@ -199,3 +200,31 @@ const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: numbe
|
||||||
|
|
||||||
return safety;
|
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 { Config } from '@roleypoly/api/src/utils/config';
|
||||||
import { Context } from '@roleypoly/api/src/utils/context';
|
import { Context } from '@roleypoly/api/src/utils/context';
|
||||||
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
|
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
|
||||||
import {
|
import { InteractionRequest, InteractionResponse } from '@roleypoly/types';
|
||||||
InteractionCallbackType,
|
|
||||||
InteractionFlags,
|
|
||||||
InteractionRequest,
|
|
||||||
InteractionResponse,
|
|
||||||
} from '@roleypoly/types';
|
|
||||||
|
|
||||||
export const verifyRequest = async (
|
export const verifyRequest = async (
|
||||||
config: Config,
|
config: Config,
|
||||||
|
@ -76,17 +71,19 @@ export const runAsync = async (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await handler(interaction, context);
|
const response = await handler(interaction, context);
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('Interaction handler returned no response');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ response });
|
||||||
|
|
||||||
await discordFetch(url, '', AuthType.None, {
|
await discordFetch(url, '', AuthType.None, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
...response.data,
|
||||||
data: {
|
|
||||||
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
|
||||||
...response.data,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -105,13 +102,18 @@ export const runAsync = async (
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
content: "I'm sorry, I'm having trouble processing this request.",
|
||||||
data: {
|
} as InteractionResponse['data']),
|
||||||
content: "I'm sorry, I'm having trouble processing this request.",
|
|
||||||
flags: InteractionFlags.EPHEMERAL,
|
|
||||||
},
|
|
||||||
} as InteractionResponse),
|
|
||||||
});
|
});
|
||||||
} catch (e) {}
|
} 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 { 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 {
|
import {
|
||||||
InteractionHandler,
|
InteractionHandler,
|
||||||
runAsync,
|
runAsync,
|
||||||
|
@ -18,6 +20,8 @@ import {
|
||||||
|
|
||||||
const commands: Record<InteractionData['name'], InteractionHandler> = {
|
const commands: Record<InteractionData['name'], InteractionHandler> = {
|
||||||
'hello-world': helloWorld,
|
'hello-world': helloWorld,
|
||||||
|
roleypoly: roleypoly,
|
||||||
|
'pickable-roles': pickableRoles,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleInteraction: RoleypolyHandler = async (
|
export const handleInteraction: RoleypolyHandler = async (
|
||||||
|
@ -57,6 +61,7 @@ export const handleInteraction: RoleypolyHandler = async (
|
||||||
return json({
|
return json({
|
||||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
|
content: 'Figuring it out...',
|
||||||
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
||||||
},
|
},
|
||||||
} as InteractionResponse);
|
} as InteractionResponse);
|
||||||
|
|
Loading…
Add table
Reference in a new issue