mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-17 18:09:09 +00:00
add majority of routes and datapaths, start on interactions
This commit is contained in:
parent
bbc0053383
commit
3033ebacb7
47 changed files with 1650 additions and 58 deletions
5
packages/api/src/guilds/audit-logging.spec.ts
Normal file
5
packages/api/src/guilds/audit-logging.spec.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
it('works', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
export {};
|
0
packages/api/src/guilds/audit-logging.ts
Normal file
0
packages/api/src/guilds/audit-logging.ts
Normal file
130
packages/api/src/guilds/getters.spec.ts
Normal file
130
packages/api/src/guilds/getters.spec.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
jest.mock('../utils/discord');
|
||||
|
||||
import { Features, GuildData } from '@roleypoly/types';
|
||||
import { APIGuild, discordFetch } from '../utils/discord';
|
||||
import { configContext } from '../utils/testHelpers';
|
||||
import { getGuild, getGuildData, getGuildMember } from './getters';
|
||||
|
||||
const mockDiscordFetch = discordFetch as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDiscordFetch.mockReset();
|
||||
});
|
||||
|
||||
describe('getGuild', () => {
|
||||
it('gets a guild from discord', async () => {
|
||||
const [config] = configContext();
|
||||
const guild = {
|
||||
id: '123',
|
||||
name: 'test',
|
||||
icon: 'test',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
mockDiscordFetch.mockReturnValue(guild);
|
||||
|
||||
const result = await getGuild(config, '123');
|
||||
|
||||
expect(result).toMatchObject(guild);
|
||||
});
|
||||
|
||||
it('gets a guild from cache automatically', async () => {
|
||||
const [config] = configContext();
|
||||
|
||||
const guild: APIGuild = {
|
||||
id: '123',
|
||||
name: 'test',
|
||||
icon: 'test',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
await config.kv.guilds.put('guilds/123', guild, config.retention.guild);
|
||||
mockDiscordFetch.mockReturnValue({ ...guild, name: 'test2' });
|
||||
|
||||
const result = await getGuild(config, '123');
|
||||
|
||||
expect(result).toMatchObject(guild);
|
||||
expect(result!.name).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGuildData', () => {
|
||||
it('gets guild data from store', async () => {
|
||||
const [config] = configContext();
|
||||
|
||||
const guildData: GuildData = {
|
||||
id: '123',
|
||||
message: 'Hello world!',
|
||||
categories: [],
|
||||
features: Features.None,
|
||||
auditLogWebhook: null,
|
||||
accessControl: {
|
||||
allowList: [],
|
||||
blockList: [],
|
||||
blockPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
await config.kv.guildData.put('123', guildData);
|
||||
|
||||
const result = await getGuildData(config, '123');
|
||||
|
||||
expect(result).toMatchObject(guildData);
|
||||
});
|
||||
|
||||
it('adds fields that are missing from the stored data', async () => {
|
||||
const [config] = configContext();
|
||||
|
||||
const guildData: Partial<GuildData> = {
|
||||
id: '123',
|
||||
message: 'Hello world!',
|
||||
categories: [],
|
||||
features: Features.None,
|
||||
};
|
||||
|
||||
await config.kv.guildData.put('123', guildData);
|
||||
const result = await getGuildData(config, '123');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
...guildData,
|
||||
auditLogWebhook: null,
|
||||
accessControl: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGuildMember', () => {
|
||||
it('gets a member from discord', async () => {
|
||||
const [config] = configContext();
|
||||
|
||||
const member = {
|
||||
roles: [],
|
||||
pending: false,
|
||||
nick: 'test',
|
||||
};
|
||||
|
||||
mockDiscordFetch.mockReturnValue(member);
|
||||
|
||||
const result = await getGuildMember(config, '123', '123');
|
||||
|
||||
expect(result).toMatchObject(member);
|
||||
});
|
||||
|
||||
it('gets a member from cache automatically', async () => {
|
||||
const [config] = configContext();
|
||||
|
||||
const member = {
|
||||
roles: [],
|
||||
pending: false,
|
||||
nick: 'test2',
|
||||
};
|
||||
|
||||
await config.kv.guilds.put('guilds/123/members/123', member, config.retention.guild);
|
||||
mockDiscordFetch.mockReturnValue({ ...member, nick: 'test' });
|
||||
|
||||
const result = await getGuildMember(config, '123', '123');
|
||||
|
||||
expect(result).toMatchObject(member);
|
||||
expect(result!.nick).toBe('test2');
|
||||
});
|
||||
});
|
149
packages/api/src/guilds/getters.ts
Normal file
149
packages/api/src/guilds/getters.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { Config } from '@roleypoly/api/src/utils/config';
|
||||
import {
|
||||
APIGuild,
|
||||
APIMember,
|
||||
APIRole,
|
||||
AuthType,
|
||||
discordFetch,
|
||||
getHighestRole,
|
||||
} from '@roleypoly/api/src/utils/discord';
|
||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||
import {
|
||||
Features,
|
||||
Guild,
|
||||
GuildData,
|
||||
Member,
|
||||
OwnRoleInfo,
|
||||
Role,
|
||||
RoleSafety,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const getGuild = async (
|
||||
config: Config,
|
||||
id: string,
|
||||
forceMiss?: boolean
|
||||
): Promise<(Guild & OwnRoleInfo) | null> =>
|
||||
config.kv.guilds.cacheThrough(
|
||||
`guilds/${id}`,
|
||||
async () => {
|
||||
const guildRaw = await discordFetch<APIGuild>(
|
||||
`/guilds/${id}`,
|
||||
config.botToken,
|
||||
AuthType.Bot
|
||||
);
|
||||
|
||||
if (!guildRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const botMemberRoles =
|
||||
(await getGuildMember(config, id, config.botClientID))?.roles || [];
|
||||
|
||||
const highestRolePosition =
|
||||
getHighestRole(
|
||||
botMemberRoles
|
||||
.map((r) => guildRaw.roles.find((r2) => r2.id === r))
|
||||
.filter((x) => x !== undefined) as APIRole[]
|
||||
)?.position || -1;
|
||||
|
||||
const roles = guildRaw.roles.map<Role>((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
managed: role.managed,
|
||||
position: role.position,
|
||||
permissions: role.permissions,
|
||||
safety: RoleSafety.Safe, // TODO: calculate this
|
||||
}));
|
||||
|
||||
const guild: Guild & OwnRoleInfo = {
|
||||
id,
|
||||
name: guildRaw.name,
|
||||
icon: guildRaw.icon,
|
||||
roles,
|
||||
highestRolePosition,
|
||||
};
|
||||
|
||||
return guild;
|
||||
},
|
||||
config.retention.guild,
|
||||
forceMiss
|
||||
);
|
||||
|
||||
export const getGuildData = async (config: Config, id: string): Promise<GuildData> => {
|
||||
const guildData = await config.kv.guildData.get<GuildData>(id);
|
||||
const empty = {
|
||||
id,
|
||||
message: '',
|
||||
categories: [],
|
||||
features: Features.None,
|
||||
auditLogWebhook: null,
|
||||
accessControl: {
|
||||
allowList: [],
|
||||
blockList: [],
|
||||
blockPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (!guildData) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return {
|
||||
...empty,
|
||||
...guildData,
|
||||
};
|
||||
};
|
||||
|
||||
export const getGuildMember = async (
|
||||
config: Config,
|
||||
serverID: string,
|
||||
userID: string,
|
||||
forceMiss?: boolean,
|
||||
overrideRetention?: number // allows for own-member to be cached as long as it's used.
|
||||
): Promise<Member | null> =>
|
||||
config.kv.guilds.cacheThrough(
|
||||
`guilds/${serverID}/members/${userID}`,
|
||||
async () => {
|
||||
const discordMember = await discordFetch<APIMember>(
|
||||
`/guilds/${serverID}/members/${userID}`,
|
||||
config.botToken,
|
||||
AuthType.Bot
|
||||
);
|
||||
|
||||
if (!discordMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
guildid: serverID,
|
||||
roles: discordMember.roles,
|
||||
pending: discordMember.pending,
|
||||
nick: discordMember.nick,
|
||||
};
|
||||
},
|
||||
overrideRetention || config.retention.member,
|
||||
forceMiss
|
||||
);
|
||||
|
||||
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
|
||||
let safety = RoleSafety.Safe;
|
||||
|
||||
if (role.managed) {
|
||||
safety |= RoleSafety.ManagedRole;
|
||||
}
|
||||
|
||||
if (role.position > highestBotRolePosition) {
|
||||
safety |= RoleSafety.HigherThanBot;
|
||||
}
|
||||
|
||||
const permBigInt = BigInt(role.permissions);
|
||||
if (
|
||||
evaluatePermission(permBigInt, permissions.ADMINISTRATOR) ||
|
||||
evaluatePermission(permBigInt, permissions.MANAGE_ROLES)
|
||||
) {
|
||||
safety |= RoleSafety.DangerousPermissions;
|
||||
}
|
||||
|
||||
return safety;
|
||||
};
|
78
packages/api/src/guilds/middleware.spec.ts
Normal file
78
packages/api/src/guilds/middleware.spec.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { Router } from 'itty-router';
|
||||
import { json } from '../utils/response';
|
||||
import { configContext, makeSession } from '../utils/testHelpers';
|
||||
import { requireEditor } from './middleware';
|
||||
|
||||
describe('requireEditor', () => {
|
||||
it('continues the request when user is an editor', async () => {
|
||||
const testFn = jest.fn();
|
||||
const [config, context] = configContext();
|
||||
const session = await makeSession(config);
|
||||
const router = Router();
|
||||
|
||||
router.all('*', requireEditor).get('/:guildId', (request, context) => {
|
||||
testFn();
|
||||
return json({});
|
||||
});
|
||||
|
||||
const response = await router.handle(
|
||||
new Request(`http://test.local/${session.guilds[1].id}`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${session.sessionID}`,
|
||||
},
|
||||
}),
|
||||
{ ...context, session, params: { guildId: session.guilds[1].id } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(testFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('403s the request when user is not an editor', async () => {
|
||||
const testFn = jest.fn();
|
||||
const [config, context] = configContext();
|
||||
const session = await makeSession(config);
|
||||
const router = Router();
|
||||
|
||||
router.all('*', requireEditor).get('/:guildId', (request, context) => {
|
||||
testFn();
|
||||
return json({});
|
||||
});
|
||||
|
||||
const response = await router.handle(
|
||||
new Request(`http://test.local/${session.guilds[0].id}`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${session.sessionID}`,
|
||||
},
|
||||
}),
|
||||
{ ...context, session, params: { guildId: session.guilds[0].id } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('404s the request when the guild isnt in session', async () => {
|
||||
const testFn = jest.fn();
|
||||
const [config, context] = configContext();
|
||||
const session = await makeSession(config);
|
||||
const router = Router();
|
||||
|
||||
router.all('*', requireEditor).get('/:guildId', (request, context) => {
|
||||
testFn();
|
||||
return json({});
|
||||
});
|
||||
|
||||
const response = await router.handle(
|
||||
new Request(`http://test.local/invalid-session-id`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${session.sessionID}`,
|
||||
},
|
||||
}),
|
||||
{ ...context, session, params: { guildId: 'invalid-session-id' } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
47
packages/api/src/guilds/middleware.ts
Normal file
47
packages/api/src/guilds/middleware.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Context, RoleypolyMiddleware } from '@roleypoly/api/src/utils/context';
|
||||
import {
|
||||
engineeringProblem,
|
||||
forbidden,
|
||||
notFound,
|
||||
} from '@roleypoly/api/src/utils/response';
|
||||
import { UserGuildPermissions } from '@roleypoly/types';
|
||||
|
||||
export const requireEditor: RoleypolyMiddleware = async (
|
||||
request: Request,
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.params.guildId) {
|
||||
return engineeringProblem('params not set up correctly');
|
||||
}
|
||||
|
||||
if (!context.session) {
|
||||
return engineeringProblem('middleware not set up correctly');
|
||||
}
|
||||
|
||||
const guild = context.session.guilds.find((g) => g.id === context.params.guildId);
|
||||
if (!guild) {
|
||||
return notFound(); // 404 because we don't want enumeration of guilds
|
||||
}
|
||||
|
||||
if (guild.permissionLevel === UserGuildPermissions.User) {
|
||||
return forbidden();
|
||||
}
|
||||
};
|
||||
|
||||
export const requireMember: RoleypolyMiddleware = async (
|
||||
request: Request,
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.params.guildId) {
|
||||
return engineeringProblem('params not set up correctly');
|
||||
}
|
||||
|
||||
if (!context.session) {
|
||||
return engineeringProblem('middleware not set up correctly');
|
||||
}
|
||||
|
||||
const guild = context.session.guilds.find((g) => g.id === context.params.guildId);
|
||||
if (!guild) {
|
||||
return notFound(); // 404 because we don't want enumeration of guilds
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue