mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
every primary path API route is refactored!
This commit is contained in:
parent
f2508fbea4
commit
d407a015c9
9 changed files with 682 additions and 12 deletions
|
@ -1,10 +1,10 @@
|
||||||
// @ts-ignore
|
|
||||||
import { requireEditor, requireMember } from '@roleypoly/api/src/guilds/middleware';
|
import { requireEditor, requireMember } from '@roleypoly/api/src/guilds/middleware';
|
||||||
import { authBot } from '@roleypoly/api/src/routes/auth/bot';
|
import { authBot } from '@roleypoly/api/src/routes/auth/bot';
|
||||||
import { authCallback } from '@roleypoly/api/src/routes/auth/callback';
|
import { authCallback } from '@roleypoly/api/src/routes/auth/callback';
|
||||||
import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session';
|
import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session';
|
||||||
import { authSession } from '@roleypoly/api/src/routes/auth/session';
|
import { authSession } from '@roleypoly/api/src/routes/auth/session';
|
||||||
import { guildsGuild } from '@roleypoly/api/src/routes/guilds/guild';
|
import { guildsGuild } from '@roleypoly/api/src/routes/guilds/guild';
|
||||||
|
import { guildsCacheDelete } from '@roleypoly/api/src/routes/guilds/guild-cache-delete';
|
||||||
import { guildsRolesPut } from '@roleypoly/api/src/routes/guilds/guild-roles-put';
|
import { guildsRolesPut } from '@roleypoly/api/src/routes/guilds/guild-roles-put';
|
||||||
import { guildsGuildPatch } from '@roleypoly/api/src/routes/guilds/guilds-patch';
|
import { guildsGuildPatch } from '@roleypoly/api/src/routes/guilds/guilds-patch';
|
||||||
import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug';
|
import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug';
|
||||||
|
@ -34,7 +34,12 @@ router.delete('/auth/session', withSession, requireSession, authSessionDelete);
|
||||||
const guildsCommon = [injectParams, withSession, requireSession, requireMember];
|
const guildsCommon = [injectParams, withSession, requireSession, requireMember];
|
||||||
router.get('/guilds/:guildId', ...guildsCommon, guildsGuild);
|
router.get('/guilds/:guildId', ...guildsCommon, guildsGuild);
|
||||||
router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch);
|
router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch);
|
||||||
router.delete('/guilds/:guildId/cache', ...guildsCommon, requireEditor, notImplemented);
|
router.delete(
|
||||||
|
'/guilds/:guildId/cache',
|
||||||
|
...guildsCommon,
|
||||||
|
requireEditor,
|
||||||
|
guildsCacheDelete
|
||||||
|
);
|
||||||
router.put('/guilds/:guildId/roles', ...guildsCommon, guildsRolesPut);
|
router.put('/guilds/:guildId/roles', ...guildsCommon, guildsRolesPut);
|
||||||
|
|
||||||
// Slug is unauthenticated...
|
// Slug is unauthenticated...
|
||||||
|
|
32
packages/api/src/routes/guilds/guild-cache-delete.spec.ts
Normal file
32
packages/api/src/routes/guilds/guild-cache-delete.spec.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
jest.mock('../../guilds/getters');
|
||||||
|
|
||||||
|
import { UserGuildPermissions } from '@roleypoly/types';
|
||||||
|
import { getGuild } from '../../guilds/getters';
|
||||||
|
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
|
||||||
|
|
||||||
|
const mockGetGuild = getGuild as jest.Mock;
|
||||||
|
|
||||||
|
describe('DELETE /guilds/:id/cache', () => {
|
||||||
|
it('calls getGuilds and returns No Content', async () => {
|
||||||
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: UserGuildPermissions.Admin,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await makeRequest('DELETE', `/guilds/123/cache`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
expect(mockGetGuild).toHaveBeenCalledWith(expect.any(Object), '123', true);
|
||||||
|
});
|
||||||
|
});
|
12
packages/api/src/routes/guilds/guild-cache-delete.ts
Normal file
12
packages/api/src/routes/guilds/guild-cache-delete.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { getGuild } from '@roleypoly/api/src/guilds/getters';
|
||||||
|
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
|
||||||
|
import { noContent } from '@roleypoly/api/src/utils/response';
|
||||||
|
|
||||||
|
export const guildsCacheDelete: RoleypolyHandler = async (
|
||||||
|
request: Request,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
await getGuild(context.config, context.params.guildId!, true);
|
||||||
|
|
||||||
|
return noContent();
|
||||||
|
};
|
|
@ -1,5 +1,370 @@
|
||||||
|
jest.mock('../../guilds/getters');
|
||||||
|
jest.mock('../../utils/discord');
|
||||||
|
|
||||||
|
import {
|
||||||
|
CategoryType,
|
||||||
|
Features,
|
||||||
|
Guild,
|
||||||
|
GuildData,
|
||||||
|
Member,
|
||||||
|
OwnRoleInfo,
|
||||||
|
RoleSafety,
|
||||||
|
RoleUpdate,
|
||||||
|
TransactionType,
|
||||||
|
} from '@roleypoly/types';
|
||||||
|
import { getGuild, getGuildData, getGuildMember } from '../../guilds/getters';
|
||||||
|
import { AuthType, discordFetch } from '../../utils/discord';
|
||||||
|
import { json } from '../../utils/response';
|
||||||
|
import { configContext, makeRequest, makeSession } from '../../utils/testHelpers';
|
||||||
|
|
||||||
|
const mockDiscordFetch = discordFetch as jest.Mock;
|
||||||
|
const mockGetGuild = getGuild as jest.Mock;
|
||||||
|
const mockGetGuildMember = getGuildMember as jest.Mock;
|
||||||
|
const mockGetGuildData = getGuildData as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
doMock();
|
||||||
|
});
|
||||||
|
|
||||||
describe('PUT /guilds/:id/roles', () => {
|
describe('PUT /guilds/:id/roles', () => {
|
||||||
it('returns Not Implemented when called', () => {
|
it('adds member roles when called with valid roles', async () => {
|
||||||
expect(true).toBe(true);
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: RoleUpdate = {
|
||||||
|
knownState: ['role-1'],
|
||||||
|
transactions: [{ id: 'role-2', action: TransactionType.Add }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockDiscordFetch.mockReturnValueOnce(
|
||||||
|
json({
|
||||||
|
roles: ['role-1', 'role-2'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await makeRequest(
|
||||||
|
'PUT',
|
||||||
|
`/guilds/123/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOT_TOKEN: 'test',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(mockDiscordFetch).toHaveBeenCalledWith(
|
||||||
|
`/guilds/123/members/${session.user.id}`,
|
||||||
|
'test',
|
||||||
|
AuthType.Bot,
|
||||||
|
{
|
||||||
|
body: JSON.stringify({
|
||||||
|
roles: ['role-1', 'role-2'],
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes member roles when called with valid roles', async () => {
|
||||||
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: RoleUpdate = {
|
||||||
|
knownState: ['role-1'],
|
||||||
|
transactions: [{ id: 'role-1', action: TransactionType.Remove }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockDiscordFetch.mockReturnValueOnce(
|
||||||
|
json({
|
||||||
|
roles: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await makeRequest(
|
||||||
|
'PUT',
|
||||||
|
`/guilds/123/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOT_TOKEN: 'test',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(mockDiscordFetch).toHaveBeenCalledWith(
|
||||||
|
`/guilds/123/members/${session.user.id}`,
|
||||||
|
'test',
|
||||||
|
AuthType.Bot,
|
||||||
|
{
|
||||||
|
body: JSON.stringify({
|
||||||
|
roles: [],
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not update roles when called only with invalid roles', async () => {
|
||||||
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: RoleUpdate = {
|
||||||
|
knownState: ['role-1'],
|
||||||
|
transactions: [
|
||||||
|
{ id: 'role-3', action: TransactionType.Add }, // role is in a hidden category
|
||||||
|
{ id: 'role-5-unsafe', action: TransactionType.Add }, // role is marked unsafe
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await makeRequest(
|
||||||
|
'PUT',
|
||||||
|
`/guilds/123/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOT_TOKEN: 'test',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(mockDiscordFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters roles that are invalid while accepting ones that are valid', async () => {
|
||||||
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: RoleUpdate = {
|
||||||
|
knownState: ['role-1'],
|
||||||
|
transactions: [
|
||||||
|
{ id: 'role-3', action: TransactionType.Add }, // role is in a hidden category
|
||||||
|
{ id: 'role-2', action: TransactionType.Add }, // role is in a hidden category
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await makeRequest(
|
||||||
|
'PUT',
|
||||||
|
`/guilds/123/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOT_TOKEN: 'test',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(mockDiscordFetch).toHaveBeenCalledWith(
|
||||||
|
`/guilds/123/members/${session.user.id}`,
|
||||||
|
'test',
|
||||||
|
AuthType.Bot,
|
||||||
|
{
|
||||||
|
body: JSON.stringify({
|
||||||
|
roles: ['role-1', 'role-2'],
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-audit-log-reason': `Picked their roles via ${config.uiPublicURI}`,
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('400s when no transactions are present', async () => {
|
||||||
|
const [config] = configContext();
|
||||||
|
const session = await makeSession(config, {
|
||||||
|
guilds: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
permissionLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: RoleUpdate = {
|
||||||
|
knownState: ['role-1'],
|
||||||
|
transactions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await makeRequest(
|
||||||
|
'PUT',
|
||||||
|
`/guilds/123/roles`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.sessionID}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BOT_TOKEN: 'test',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(mockDiscordFetch).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetGuild).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetGuildData).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetGuildMember).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const doMock = () => {
|
||||||
|
const guild: Guild & OwnRoleInfo = {
|
||||||
|
id: '123',
|
||||||
|
name: 'test',
|
||||||
|
icon: 'test',
|
||||||
|
highestRolePosition: 0,
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
id: 'role-1',
|
||||||
|
name: 'Role 1',
|
||||||
|
color: 0,
|
||||||
|
position: 17,
|
||||||
|
permissions: '',
|
||||||
|
managed: false,
|
||||||
|
safety: RoleSafety.Safe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-2',
|
||||||
|
name: 'Role 2',
|
||||||
|
color: 0,
|
||||||
|
position: 16,
|
||||||
|
permissions: '',
|
||||||
|
managed: false,
|
||||||
|
safety: RoleSafety.Safe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-3',
|
||||||
|
name: 'Role 3',
|
||||||
|
color: 0,
|
||||||
|
position: 15,
|
||||||
|
permissions: '',
|
||||||
|
managed: false,
|
||||||
|
safety: RoleSafety.Safe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-4',
|
||||||
|
name: 'Role 4',
|
||||||
|
color: 0,
|
||||||
|
position: 14,
|
||||||
|
permissions: '',
|
||||||
|
managed: false,
|
||||||
|
safety: RoleSafety.Safe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role-5-unsafe',
|
||||||
|
name: 'Role 5 (Unsafe)',
|
||||||
|
color: 0,
|
||||||
|
position: 14,
|
||||||
|
permissions: '',
|
||||||
|
managed: false,
|
||||||
|
safety: RoleSafety.DangerousPermissions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const member: Member = {
|
||||||
|
roles: ['role-1'],
|
||||||
|
pending: false,
|
||||||
|
nick: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const guildData: GuildData = {
|
||||||
|
id: '123',
|
||||||
|
message: 'test',
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
id: 'category-1',
|
||||||
|
name: 'Category 1',
|
||||||
|
position: 0,
|
||||||
|
hidden: false,
|
||||||
|
type: CategoryType.Multi,
|
||||||
|
roles: ['role-1', 'role-2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'category-2',
|
||||||
|
name: 'Category 2',
|
||||||
|
position: 1,
|
||||||
|
hidden: true,
|
||||||
|
type: CategoryType.Multi,
|
||||||
|
roles: ['role-3'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
features: Features.None,
|
||||||
|
auditLogWebhook: null,
|
||||||
|
accessControl: {
|
||||||
|
allowList: [],
|
||||||
|
blockList: [],
|
||||||
|
blockPending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockGetGuild.mockReturnValue(guild);
|
||||||
|
mockGetGuildMember.mockReturnValue(member);
|
||||||
|
mockGetGuildData.mockReturnValue(guildData);
|
||||||
|
mockDiscordFetch.mockReturnValue(json({}));
|
||||||
|
};
|
||||||
|
|
|
@ -1,9 +1,154 @@
|
||||||
|
import {
|
||||||
|
getGuild,
|
||||||
|
getGuildData,
|
||||||
|
getGuildMember,
|
||||||
|
} from '@roleypoly/api/src/guilds/getters';
|
||||||
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
|
import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context';
|
||||||
import { notImplemented } from '@roleypoly/api/src/utils/response';
|
import { AuthType, discordFetch } from '@roleypoly/api/src/utils/discord';
|
||||||
|
import {
|
||||||
|
engineeringProblem,
|
||||||
|
invalid,
|
||||||
|
json,
|
||||||
|
notFound,
|
||||||
|
serverError,
|
||||||
|
} from '@roleypoly/api/src/utils/response';
|
||||||
|
import {
|
||||||
|
difference,
|
||||||
|
isIdenticalArray,
|
||||||
|
keyBy,
|
||||||
|
union,
|
||||||
|
} from '@roleypoly/misc-utils/collection-tools';
|
||||||
|
import {
|
||||||
|
GuildData,
|
||||||
|
Member,
|
||||||
|
Role,
|
||||||
|
RoleSafety,
|
||||||
|
RoleTransaction,
|
||||||
|
RoleUpdate,
|
||||||
|
TransactionType,
|
||||||
|
} from '@roleypoly/types';
|
||||||
|
|
||||||
export const guildsRolesPut: RoleypolyHandler = async (
|
export const guildsRolesPut: RoleypolyHandler = async (
|
||||||
request: Request,
|
request: Request,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
return notImplemented();
|
if (!request.body) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRequest: RoleUpdate = await request.json();
|
||||||
|
|
||||||
|
if (updateRequest.transactions.length === 0) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildID = context.params.guildId;
|
||||||
|
if (!guildID) {
|
||||||
|
return engineeringProblem('params not set up correctly');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userID = context.session!.user.id;
|
||||||
|
|
||||||
|
const [member, guildData, guild] = await Promise.all([
|
||||||
|
getGuildMember(context.config, guildID, userID),
|
||||||
|
getGuildData(context.config, guildID),
|
||||||
|
getGuild(context.config, guildID),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!guild || !member) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoles = calculateNewRoles({
|
||||||
|
currentRoles: member.roles,
|
||||||
|
guildRoles: guild.roles,
|
||||||
|
guildData,
|
||||||
|
updateRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isIdenticalArray(member.roles, newRoles)) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchMemberRoles = await discordFetch<Member>(
|
||||||
|
`/guilds/${guildID}/members/${userID}`,
|
||||||
|
context.config.botToken,
|
||||||
|
AuthType.Bot,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-audit-log-reason': `Picked their roles via ${context.config.uiPublicURI}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
roles: newRoles,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!patchMemberRoles) {
|
||||||
|
return serverError(new Error('discord rejected the request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fetchContext.waitUntil(getGuildMember(context.config, guildID, userID, true));
|
||||||
|
|
||||||
|
const updatedMember: Member = {
|
||||||
|
roles: patchMemberRoles.roles,
|
||||||
|
};
|
||||||
|
|
||||||
|
return json(updatedMember);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateNewRoles = ({
|
||||||
|
currentRoles,
|
||||||
|
guildData,
|
||||||
|
guildRoles,
|
||||||
|
updateRequest,
|
||||||
|
}: {
|
||||||
|
currentRoles: string[];
|
||||||
|
guildRoles: Role[];
|
||||||
|
guildData: GuildData;
|
||||||
|
updateRequest: RoleUpdate;
|
||||||
|
}): string[] => {
|
||||||
|
const roleMap = keyBy(guildRoles, 'id');
|
||||||
|
|
||||||
|
// These roles were ones changed between knownState (role picker page load/cache) and current (fresh from discord).
|
||||||
|
// We could cause issues, so we'll re-add them later.
|
||||||
|
// const diffRoles = difference(updateRequest.knownState, currentRoles);
|
||||||
|
|
||||||
|
// Only these are safe
|
||||||
|
const allSafeRoles = guildData.categories.reduce<string[]>(
|
||||||
|
(categorizedRoles, category) =>
|
||||||
|
!category.hidden
|
||||||
|
? [
|
||||||
|
...categorizedRoles,
|
||||||
|
...category.roles.filter(
|
||||||
|
(roleID) => roleMap[roleID]?.safety === RoleSafety.Safe
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: categorizedRoles,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) =>
|
||||||
|
allSafeRoles.includes(tx.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const changesByAction = safeTransactions.reduce<
|
||||||
|
Record<TransactionType, RoleTransaction[]>
|
||||||
|
>((group, value, _1, _2, key = value.action) => (group[key].push(value), group), {
|
||||||
|
[TransactionType.Add]: [],
|
||||||
|
[TransactionType.Remove]: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map(
|
||||||
|
(tx: RoleTransaction) => tx.id
|
||||||
|
);
|
||||||
|
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
|
||||||
|
(tx: RoleTransaction) => tx.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
|
||||||
|
|
||||||
|
return final;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
jest.mock('../../../utils/discord');
|
||||||
|
|
||||||
|
import { discordFetch } from '../../../utils/discord';
|
||||||
|
import { configContext } from '../../../utils/testHelpers';
|
||||||
|
import {
|
||||||
|
extractInteractionResponse,
|
||||||
|
isDeferred,
|
||||||
|
isEphemeral,
|
||||||
|
makeInteractionsRequest,
|
||||||
|
mockUpdateCall,
|
||||||
|
} from '../testHelpers';
|
||||||
|
|
||||||
|
const mockDiscordFetch = discordFetch as jest.Mock;
|
||||||
|
it('responds with the username when member.nick is missing', async () => {
|
||||||
|
const [, context] = configContext();
|
||||||
|
const response = await makeInteractionsRequest(
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
name: 'hello-world',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
member: {
|
||||||
|
nick: undefined,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const interaction = await extractInteractionResponse(response);
|
||||||
|
|
||||||
|
expect(isDeferred(interaction)).toBe(true);
|
||||||
|
expect(isEphemeral(interaction)).toBe(true);
|
||||||
|
expect(mockDiscordFetch).toBeCalledWith(
|
||||||
|
...mockUpdateCall(expect, {
|
||||||
|
content: 'Hey there, test-user',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('responds with the nickname when member.nick is set', async () => {
|
||||||
|
const [, context] = configContext();
|
||||||
|
const response = await makeInteractionsRequest(context, {
|
||||||
|
name: 'hello-world',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const interaction = await extractInteractionResponse(response);
|
||||||
|
|
||||||
|
expect(isDeferred(interaction)).toBe(true);
|
||||||
|
expect(isEphemeral(interaction)).toBe(true);
|
||||||
|
expect(mockDiscordFetch).toBeCalledWith(
|
||||||
|
...mockUpdateCall(expect, {
|
||||||
|
content: 'Hey there, test-user-nick',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
|
@ -59,7 +59,7 @@ 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,
|
type: InteractionCallbackType.DEFERRED_UPDATE_MESSAGE,
|
||||||
data: {
|
data: {
|
||||||
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
flags: handler.ephemeral ? InteractionFlags.EPHEMERAL : 0,
|
||||||
...response.data,
|
...response.data,
|
||||||
|
@ -82,7 +82,7 @@ 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,
|
type: InteractionCallbackType.DEFERRED_UPDATE_MESSAGE,
|
||||||
data: {
|
data: {
|
||||||
content: "I'm sorry, I'm having trouble processing this request.",
|
content: "I'm sorry, I'm having trouble processing this request.",
|
||||||
flags: InteractionFlags.EPHEMERAL,
|
flags: InteractionFlags.EPHEMERAL,
|
||||||
|
|
|
@ -24,10 +24,10 @@ it('responds with a simple hello-world!', async () => {
|
||||||
});
|
});
|
||||||
expect(mockDiscordFetch).toBeCalledWith(expect.any(String), '', AuthType.None, {
|
expect(mockDiscordFetch).toBeCalledWith(expect.any(String), '', AuthType.None, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionCallbackType.DEFERRED_UPDATE_MESSAGE,
|
||||||
data: {
|
data: {
|
||||||
flags: InteractionFlags.EPHEMERAL,
|
flags: InteractionFlags.EPHEMERAL,
|
||||||
content: 'Hey there, test-user',
|
content: 'Hey there, test-user-nick',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
headers: expect.any(Object),
|
headers: expect.any(Object),
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions';
|
import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions';
|
||||||
import { Context } from '@roleypoly/api/src/utils/context';
|
import { Context } from '@roleypoly/api/src/utils/context';
|
||||||
|
import { AuthType } from '@roleypoly/api/src/utils/discord';
|
||||||
import { getID } from '@roleypoly/api/src/utils/id';
|
import { getID } from '@roleypoly/api/src/utils/id';
|
||||||
import {
|
import {
|
||||||
|
InteractionCallbackData,
|
||||||
|
InteractionCallbackType,
|
||||||
InteractionData,
|
InteractionData,
|
||||||
|
InteractionFlags,
|
||||||
InteractionRequest,
|
InteractionRequest,
|
||||||
InteractionResponse,
|
InteractionResponse,
|
||||||
InteractionType,
|
InteractionType,
|
||||||
|
@ -32,7 +36,8 @@ export const getSignatureHeaders = (
|
||||||
export const makeInteractionsRequest = async (
|
export const makeInteractionsRequest = async (
|
||||||
context: Context,
|
context: Context,
|
||||||
interactionData: Partial<InteractionData>,
|
interactionData: Partial<InteractionData>,
|
||||||
forceInvalid?: boolean
|
forceInvalid?: boolean,
|
||||||
|
topLevelMixin?: Partial<InteractionRequest>
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
context.config.publicKey = hexPublicKey;
|
context.config.publicKey = hexPublicKey;
|
||||||
|
|
||||||
|
@ -55,9 +60,10 @@ export const makeInteractionsRequest = async (
|
||||||
avatar: '',
|
avatar: '',
|
||||||
},
|
},
|
||||||
member: {
|
member: {
|
||||||
nick: 'test-user',
|
nick: 'test-user-nick',
|
||||||
roles: [],
|
roles: [],
|
||||||
},
|
},
|
||||||
|
...topLevelMixin,
|
||||||
};
|
};
|
||||||
|
|
||||||
const request = new Request('http://localhost:3000/interactions', {
|
const request = new Request('http://localhost:3000/interactions', {
|
||||||
|
@ -81,3 +87,48 @@ export const extractInteractionResponse = async (
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
return body as InteractionResponse;
|
return body as InteractionResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isDeferred = (response: InteractionResponse): boolean => {
|
||||||
|
return response.type === InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEphemeral = (response: InteractionResponse): boolean => {
|
||||||
|
return (
|
||||||
|
(response.data?.flags || 0 & InteractionFlags.EPHEMERAL) ===
|
||||||
|
InteractionFlags.EPHEMERAL
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const interactionData = (
|
||||||
|
response: InteractionResponse
|
||||||
|
): Omit<InteractionCallbackData, 'flags'> | undefined => {
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) return undefined;
|
||||||
|
|
||||||
|
delete data.flags;
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockUpdateCall = (
|
||||||
|
expect: any,
|
||||||
|
data: Omit<InteractionCallbackData, 'flags'>
|
||||||
|
) => {
|
||||||
|
return [
|
||||||
|
expect.any(String),
|
||||||
|
'',
|
||||||
|
AuthType.None,
|
||||||
|
{
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: InteractionCallbackType.DEFERRED_UPDATE_MESSAGE,
|
||||||
|
data: {
|
||||||
|
flags: InteractionFlags.EPHEMERAL,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue