feat(api): add get-picker-data; refactor fully away from old gRPC datatypes

This commit is contained in:
41666 2020-12-15 21:32:46 -05:00
parent 823760dc2f
commit 0b384bfe5c
22 changed files with 359 additions and 151 deletions

View file

@ -63,7 +63,7 @@ const server = http.createServer((req, res) => {
} }
res.statusCode = response.status; res.statusCode = response.status;
loggedStatus = String(response.status); loggedStatus = String(response.status);
Object.entries(response.headers).forEach(([k, v]) => res.setHeader(k, v)); response.headers.forEach((value, key) => res.setHeader(key, value));
res.end(response.body); res.end(response.body);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View file

@ -0,0 +1,78 @@
import {
DiscordUser,
GuildSlug,
PresentableGuild,
SessionData,
} from 'roleypoly/common/types';
import { respond, withSession } from '../utils/api-tools';
import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild';
const fail = respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
export const GetPickerData = withSession(
(session?: SessionData) => async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const [, , guildID] = url.pathname.split('/');
if (!guildID) {
return respond(
{
error: 'missing guild id',
},
{
status: 400,
}
);
}
const { id: userID } = session?.user as DiscordUser;
const guilds = session?.guilds as GuildSlug[];
// Save a Discord API request by checking if this user is a member by session first
const checkGuild = guilds.find((guild) => guild.id === guildID);
if (!checkGuild) {
return fail;
}
const guild = await getGuild(guildID, {
skipCachePull: url.searchParams.has('__no_cache'),
});
if (!guild) {
return fail;
}
const memberRolesP = getGuildMemberRoles({
serverID: guildID,
userID,
});
const guildDataP = getGuildData(guildID);
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
if (!memberRoles) {
return fail;
}
const presentableGuild: PresentableGuild = {
id: guildID,
guild: checkGuild,
roles: guild.roles,
member: {
roles: memberRoles,
},
data: guildData,
};
return respond(presentableGuild);
},
{
mustAuthenticate: true,
}
);

View file

@ -120,11 +120,15 @@ const getGuilds = async (accessToken: string) => {
'Bearer' 'Bearer'
); );
if (!guilds) {
return [];
}
const guildSlugs = guilds.map<GuildSlug>((guild) => ({ const guildSlugs = guilds.map<GuildSlug>((guild) => ({
id: guild.id, id: guild.id,
name: guild.name, name: guild.name,
icon: guild.icon, icon: guild.icon,
permissionLevel: parsePermissions(guild.permissions, guild.owner), permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
})); }));
return guildSlugs; return guildSlugs;

View file

@ -1,4 +1,5 @@
import { BotJoin } from './handlers/bot-join'; import { BotJoin } from './handlers/bot-join';
import { GetPickerData } from './handlers/get-picker-data';
import { GetSession } from './handlers/get-session'; import { GetSession } from './handlers/get-session';
import { GetSlug } from './handlers/get-slug'; import { GetSlug } from './handlers/get-slug';
import { LoginBounce } from './handlers/login-bounce'; import { LoginBounce } from './handlers/login-bounce';
@ -16,6 +17,7 @@ router.add('GET', 'login-bounce', LoginBounce);
router.add('GET', 'login-callback', LoginCallback); router.add('GET', 'login-callback', LoginCallback);
router.add('GET', 'get-session', GetSession); router.add('GET', 'get-session', GetSession);
router.add('GET', 'get-slug', GetSlug); router.add('GET', 'get-slug', GetSlug);
router.add('GET', 'get-picker-data', GetPickerData);
router.add('GET', 'x-headers', (request) => { router.add('GET', 'x-headers', (request) => {
const headers: { [x: string]: string } = {}; const headers: { [x: string]: string } = {};

View file

@ -28,7 +28,7 @@ export const resolveFailures = (
}; };
export const parsePermissions = ( export const parsePermissions = (
permissions: number, permissions: bigint,
owner: boolean = false owner: boolean = false
): UserGuildPermissions => { ): UserGuildPermissions => {
if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) { if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) {
@ -56,6 +56,9 @@ export const getSessionID = (request: Request): { type: string; id: string } | n
return { type, id }; return { type, id };
}; };
const userAgent =
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';
export const discordFetch = async <T>( export const discordFetch = async <T>(
url: string, url: string,
auth: string, auth: string,
@ -64,8 +67,7 @@ export const discordFetch = async <T>(
const response = await fetch('https://discord.com/api/v8' + url, { const response = await fetch('https://discord.com/api/v8' + url, {
headers: { headers: {
authorization: `${authType} ${auth}`, authorization: `${authType} ${auth}`,
'user-agent': 'user-agent': userAgent,
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)',
}, },
}); });
@ -81,12 +83,17 @@ export const cacheLayer = <Identity, Data>(
keyFactory: (identity: Identity) => string, keyFactory: (identity: Identity) => string,
missHandler: (identity: Identity) => Promise<Data | null>, missHandler: (identity: Identity) => Promise<Data | null>,
ttlSeconds?: number ttlSeconds?: number
) => async (identity: Identity): Promise<Data | null> => { ) => async (
identity: Identity,
options: { skipCachePull?: boolean } = {}
): Promise<Data | null> => {
const key = keyFactory(identity); const key = keyFactory(identity);
const value = await kv.get<Data>(key); if (!options.skipCachePull) {
if (value) { const value = await kv.get<Data>(key);
return value; if (value) {
return value;
}
} }
const fallbackValue = await missHandler(identity); const fallbackValue = await missHandler(identity);

View file

@ -1,7 +1,15 @@
import { Guild, Member, Role, RoleSafety } from 'roleypoly/common/types'; import {
Features,
Guild,
GuildData as GuildDataT,
OwnRoleInfo,
Role,
RoleSafety,
} from 'roleypoly/common/types';
import { evaluatePermission, permissions } from 'roleypoly/common/utils/hasPermission';
import { cacheLayer, discordFetch } from './api-tools'; import { cacheLayer, discordFetch } from './api-tools';
import { botToken } from './config'; import { botClientID, botToken } from './config';
import { Guilds } from './kv'; import { GuildData, Guilds } from './kv';
type APIGuild = { type APIGuild = {
// Only relevant stuff // Only relevant stuff
@ -30,24 +38,112 @@ export const getGuild = cacheLayer(
return null; return null;
} }
const botMemberRoles =
(await getGuildMemberRoles({
serverID: id,
userID: botClientID,
})) || [];
const highestRolePosition = botMemberRoles.reduce<number>((highest, roleID) => {
const role = guildRaw.roles.find((guildRole) => guildRole.id === roleID);
if (!role) {
return highest;
}
// If highest is a bigger number, it stays the highest.
if (highest > role.position) {
return highest;
}
return role.position;
}, guildRaw.roles.length - 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: calculateRoleSafety(role, highestRolePosition),
}));
// Filters the raw guild data into data we actually want // Filters the raw guild data into data we actually want
const guild: Guild = { const guild: Guild & OwnRoleInfo = {
id: guildRaw.id, id: guildRaw.id,
name: guildRaw.name, name: guildRaw.name,
icon: guildRaw.icon, icon: guildRaw.icon,
roles: guildRaw.roles.map<Role>((role) => ({ roles,
...role, highestRolePosition,
safety: RoleSafety.SAFE, // TODO: calculate safety
})),
}; };
return guild; return guild;
} },
60 * 60 * 2 // 2 hour TTL
); );
export const getGuildMember = async ( type GuildMemberIdentity = {
serverID: string, serverID: string;
userID: string userID: string;
): Promise<Member> => { };
return {} as any;
type APIMember = {
// Only relevant stuff, again.
roles: string[];
};
export const getGuildMemberRoles = cacheLayer<GuildMemberIdentity, Role['id'][]>(
Guilds,
({ serverID, userID }) => `guilds/${serverID}/members/${userID}`,
async ({ serverID, userID }) => {
const discordMember = await discordFetch<APIMember>(
`/guilds/${serverID}/members/${userID}`,
botToken,
'Bot'
);
if (!discordMember) {
return null;
}
return discordMember.roles;
},
60 * 5 // 5 minute TTL
);
export const getGuildData = async (id: string): Promise<GuildDataT> => {
const guildData = await GuildData.get<GuildDataT>(id);
if (!guildData) {
return {
id,
message: '',
categories: [],
features: Features.None,
};
}
return guildData;
};
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;
}; };

View file

@ -1,12 +1,12 @@
export enum CategoryType { export enum CategoryType {
SINGLE = 0, Single = 0,
MULTI, Multi,
} }
export type Category = { export type Category = {
id: string; id: string;
name: string; name: string;
rolesList: string[]; roles: string[];
hidden: boolean; hidden: boolean;
type: CategoryType; type: CategoryType;
position: number; position: number;

View file

@ -9,34 +9,34 @@ export type Guild = {
roles: Role[]; roles: Role[];
}; };
export type GuildRoles = { export enum Features {
id: string; None,
rolesList: Role[]; Preview = None,
}; }
export type GuildData = { export type GuildData = {
id: string; id: string;
message: string; message: string;
categoriesList: Category[]; categories: Category[];
entitlementsList: string[]; features: Features;
}; };
export type PresentableGuild = { export type PresentableGuild = {
id: string; id: string;
guild: Guild; guild: GuildSlug;
member: Member; member: Member;
data: GuildData; data: GuildData;
roles: GuildRoles; roles: Role[];
}; };
export type GuildEnumeration = { export type GuildEnumeration = {
guildsList: PresentableGuild[]; guilds: PresentableGuild[];
}; };
export enum UserGuildPermissions { export enum UserGuildPermissions {
User, User,
Manager, Manager = 1 << 1,
Admin, Admin = 1 << 2,
} }
export type GuildSlug = { export type GuildSlug = {

View file

@ -1,7 +1,8 @@
export enum RoleSafety { export enum RoleSafety {
SAFE = 0, Safe = 0,
HIGHERTHANBOT, HigherThanBot = 1 << 1,
DANGEROUSPERMISSIONS, DangerousPermissions = 1 << 2,
ManagedRole = 1 << 3,
} }
export type Role = { export type Role = {
@ -14,3 +15,7 @@ export type Role = {
/** Permissions is should be used as a BigInt, NOT a number. */ /** Permissions is should be used as a BigInt, NOT a number. */
permissions: string; permissions: string;
}; };
export type OwnRoleInfo = {
highestRolePosition: number;
};

View file

@ -7,10 +7,10 @@ export type DiscordUser = {
}; };
export type Member = { export type Member = {
guildid: string; guildid?: string;
rolesList: string[]; roles: string[];
nick: string; nick?: string;
user: DiscordUser; user?: DiscordUser;
}; };
export type RoleypolyUser = { export type RoleypolyUser = {

View file

@ -2,10 +2,10 @@ import {
Category, Category,
CategoryType, CategoryType,
DiscordUser, DiscordUser,
Features,
Guild, Guild,
GuildData, GuildData,
GuildEnumeration, GuildEnumeration,
GuildRoles,
GuildSlug, GuildSlug,
Member, Member,
Role, Role,
@ -21,7 +21,7 @@ export const roleCategory: Role[] = [
color: 0xffc0cb, color: 0xffc0cb,
position: 1, position: 1,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'bbb', id: 'bbb',
@ -30,7 +30,7 @@ export const roleCategory: Role[] = [
color: 0xc0ebff, color: 0xc0ebff,
position: 2, position: 2,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'ccc', id: 'ccc',
@ -39,7 +39,7 @@ export const roleCategory: Role[] = [
color: 0xc0ffd5, color: 0xc0ffd5,
position: 3, position: 3,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'ddd', id: 'ddd',
@ -48,7 +48,7 @@ export const roleCategory: Role[] = [
color: 0xff0000, color: 0xff0000,
position: 4, position: 4,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'eee', id: 'eee',
@ -57,7 +57,7 @@ export const roleCategory: Role[] = [
color: 0x000000, color: 0x000000,
position: 5, position: 5,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'fff', id: 'fff',
@ -66,7 +66,7 @@ export const roleCategory: Role[] = [
color: 0x1, color: 0x1,
position: 6, position: 6,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'unsafe1', id: 'unsafe1',
@ -75,7 +75,7 @@ export const roleCategory: Role[] = [
color: 0xff0088, color: 0xff0088,
position: 7, position: 7,
managed: false, managed: false,
safety: RoleSafety.HIGHERTHANBOT, safety: RoleSafety.HigherThanBot,
}, },
{ {
id: 'unsafe2', id: 'unsafe2',
@ -84,16 +84,16 @@ export const roleCategory: Role[] = [
color: 0x00ff88, color: 0x00ff88,
position: 8, position: 8,
managed: false, managed: false,
safety: RoleSafety.DANGEROUSPERMISSIONS, safety: RoleSafety.DangerousPermissions,
}, },
]; ];
export const mockCategory: Category = { export const mockCategory: Category = {
id: 'aaa', id: 'aaa',
name: 'Mock', name: 'Mock',
rolesList: roleCategory.map((x) => x.id), roles: roleCategory.map((x) => x.id),
hidden: false, hidden: false,
type: CategoryType.MULTI, type: CategoryType.Multi,
position: 0, position: 0,
}; };
@ -105,7 +105,7 @@ export const roleCategory2: Role[] = [
color: 0xff0000, color: 0xff0000,
position: 9, position: 9,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
{ {
id: 'eee2', id: 'eee2',
@ -114,24 +114,19 @@ export const roleCategory2: Role[] = [
color: 0x00ff00, color: 0x00ff00,
position: 10, position: 10,
managed: false, managed: false,
safety: RoleSafety.SAFE, safety: RoleSafety.Safe,
}, },
]; ];
export const mockCategorySingle: Category = { export const mockCategorySingle: Category = {
id: 'bbb', id: 'bbb',
name: 'Mock Single 岡野', name: 'Mock Single 岡野',
rolesList: roleCategory2.map((x) => x.id), roles: roleCategory2.map((x) => x.id),
hidden: false, hidden: false,
type: CategoryType.SINGLE, type: CategoryType.Single,
position: 0, position: 0,
}; };
export const guildRoles: GuildRoles = {
id: 'aaa',
rolesList: [...roleCategory, ...roleCategory2],
};
export const roleWikiData = { export const roleWikiData = {
aaa: 'Typically used by feminine-identifying people', aaa: 'Typically used by feminine-identifying people',
bbb: 'Typically used by masculine-identifying people', bbb: 'Typically used by masculine-identifying people',
@ -145,33 +140,38 @@ export const guild: Guild = {
roles: [], roles: [],
}; };
export const guildMap: { [x: string]: Guild } = { export const guildMap: { [x: string]: GuildSlug } = {
'emoji megaporium': guild, 'emoji megaporium': {
name: guild.name,
id: guild.id,
permissionLevel: 0,
icon: guild.icon,
},
Roleypoly: { Roleypoly: {
name: 'Roleypoly', name: 'Roleypoly',
id: '203493697696956418', id: '203493697696956418',
permissionLevel: 0,
icon: 'ff08d36f5aee1ff48f8377b65d031ab0', icon: 'ff08d36f5aee1ff48f8377b65d031ab0',
roles: [],
}, },
'chamber of secrets': { 'chamber of secrets': {
name: 'chamber of secrets', name: 'chamber of secrets',
id: 'aaa', id: 'aaa',
permissionLevel: 0,
icon: '', icon: '',
roles: [],
}, },
Eclipse: { Eclipse: {
name: 'Eclipse', name: 'Eclipse',
id: '408821059161423873', id: '408821059161423873',
permissionLevel: 0,
icon: '49dfdd8b2456e2977e80a8b577b19c0d', icon: '49dfdd8b2456e2977e80a8b577b19c0d',
roles: [],
}, },
}; };
export const guildData: GuildData = { export const guildData: GuildData = {
id: 'aaa', id: 'aaa',
message: 'henlo worl!!', message: 'henlo worl!!',
categoriesList: [mockCategory, mockCategorySingle], categories: [mockCategory, mockCategorySingle],
entitlementsList: [], features: Features.None,
}; };
export const user: DiscordUser = { export const user: DiscordUser = {
@ -184,7 +184,7 @@ export const user: DiscordUser = {
export const member: Member = { export const member: Member = {
guildid: 'aaa', guildid: 'aaa',
rolesList: ['aaa', 'eee', 'unsafe2', 'ddd2'], roles: ['aaa', 'eee', 'unsafe2', 'ddd2'],
nick: 'okano cat', nick: 'okano cat',
user: user, user: user,
}; };
@ -194,42 +194,42 @@ export const rpUser: RoleypolyUser = {
}; };
export const guildEnum: GuildEnumeration = { export const guildEnum: GuildEnumeration = {
guildsList: [ guilds: [
{ {
id: 'aaa', id: 'aaa',
guild: guildMap['emoji megaporium'], guild: guildMap['emoji megaporium'],
member, member,
data: guildData, data: guildData,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
}, },
{ {
id: 'bbb', id: 'bbb',
guild: guildMap['Roleypoly'], guild: guildMap['Roleypoly'],
member: { member: {
...member, ...member,
rolesList: ['unsafe2'], roles: ['unsafe2'],
}, },
data: guildData, data: guildData,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
}, },
{ {
id: 'ccc', id: 'ccc',
guild: guildMap['chamber of secrets'], guild: guildMap['chamber of secrets'],
member, member,
data: guildData, data: guildData,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
}, },
{ {
id: 'ddd', id: 'ddd',
guild: guildMap['Eclipse'], guild: guildMap['Eclipse'],
member, member,
data: guildData, data: guildData,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
}, },
], ],
}; };
export const mastheadSlugs: GuildSlug[] = guildEnum.guildsList.map<GuildSlug>( export const mastheadSlugs: GuildSlug[] = guildEnum.guilds.map<GuildSlug>(
(guild, idx) => ({ (guild, idx) => ({
id: guild.guild.id, id: guild.guild.id,
name: guild.guild.name, name: guild.guild.name,

View file

@ -1,20 +1,29 @@
import { Role } from 'roleypoly/common/types'; import { Role } from 'roleypoly/common/types';
import { guildRoles } from 'roleypoly/common/types/storyData'; import { roleCategory } from 'roleypoly/common/types/storyData';
import { hasPermission, hasPermissionOrAdmin, permissions } from './hasPermission'; import { hasPermission, hasPermissionOrAdmin } from './hasPermission';
export const permissions = {
KICK_MEMBERS: BigInt(0x2),
BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8),
SPEAK: BigInt(0x200000),
CHANGE_NICKNAME: BigInt(0x4000000),
MANAGE_ROLES: BigInt(0x10000000),
};
const roles: Role[] = [ const roles: Role[] = [
{ {
...guildRoles.rolesList[0], ...roleCategory[0],
permissions: String(permissions.ADMINISTRATOR), permissions: String(permissions.ADMINISTRATOR),
}, },
{ {
...guildRoles.rolesList[0], ...roleCategory[0],
permissions: String( permissions: String(
permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME
), ),
}, },
{ {
...guildRoles.rolesList[0], ...roleCategory[0],
permissions: String(permissions.BAN_MEMBERS), permissions: String(permissions.BAN_MEMBERS),
}, },
]; ];

View file

@ -19,35 +19,37 @@ export const hasPermissionOrAdmin = (roles: Role[], permission: bigint): boolean
hasPermission(roles, permission | permissions.ADMINISTRATOR); hasPermission(roles, permission | permissions.ADMINISTRATOR);
export const permissions = { export const permissions = {
CREATE_INSTANT_INVITE: BigInt(0x1), // IMPORTANT: Only uncomment what's actually used. All are left for convenience.
KICK_MEMBERS: BigInt(0x2),
BAN_MEMBERS: BigInt(0x4), // CREATE_INSTANT_INVITE: BigInt(0x1),
// KICK_MEMBERS: BigInt(0x2),
// BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8), ADMINISTRATOR: BigInt(0x8),
MANAGE_CHANNELS: BigInt(0x10), // MANAGE_CHANNELS: BigInt(0x10),
MANAGE_GUILD: BigInt(0x20), // MANAGE_GUILD: BigInt(0x20),
ADD_REACTIONS: BigInt(0x40), // ADD_REACTIONS: BigInt(0x40),
VIEW_AUDIT_LOG: BigInt(0x80), // VIEW_AUDIT_LOG: BigInt(0x80),
VIEW_CHANNEL: BigInt(0x400), // VIEW_CHANNEL: BigInt(0x400),
SEND_MESSAGES: BigInt(0x800), // SEND_MESSAGES: BigInt(0x800),
SEND_TTS_MESSAGES: BigInt(0x1000), // SEND_TTS_MESSAGES: BigInt(0x1000),
MANAGE_MESSAGES: BigInt(0x2000), // MANAGE_MESSAGES: BigInt(0x2000),
EMBED_LINKS: BigInt(0x4000), // EMBED_LINKS: BigInt(0x4000),
ATTACH_FILES: BigInt(0x8000), // ATTACH_FILES: BigInt(0x8000),
READ_MESSAGE_HISTORY: BigInt(0x10000), // READ_MESSAGE_HISTORY: BigInt(0x10000),
MENTION_EVERYONE: BigInt(0x20000), // MENTION_EVERYONE: BigInt(0x20000),
USE_EXTERNAL_EMOJIS: BigInt(0x40000), // USE_EXTERNAL_EMOJIS: BigInt(0x40000),
VIEW_GUILD_INSIGHTS: BigInt(0x80000), // VIEW_GUILD_INSIGHTS: BigInt(0x80000),
CONNECT: BigInt(0x100000), // CONNECT: BigInt(0x100000),
SPEAK: BigInt(0x200000), // SPEAK: BigInt(0x200000),
MUTE_MEMBERS: BigInt(0x400000), // MUTE_MEMBERS: BigInt(0x400000),
DEAFEN_MEMBERS: BigInt(0x800000), // DEAFEN_MEMBERS: BigInt(0x800000),
MOVE_MEMBERS: BigInt(0x1000000), // MOVE_MEMBERS: BigInt(0x1000000),
USE_VAD: BigInt(0x2000000), // USE_VAD: BigInt(0x2000000),
PRIORITY_SPEAKER: BigInt(0x100), // PRIORITY_SPEAKER: BigInt(0x100),
STREAM: BigInt(0x200), // STREAM: BigInt(0x200),
CHANGE_NICKNAME: BigInt(0x4000000), // CHANGE_NICKNAME: BigInt(0x4000000),
MANAGE_NICKNAMES: BigInt(0x8000000), // MANAGE_NICKNAMES: BigInt(0x8000000),
MANAGE_ROLES: BigInt(0x10000000), MANAGE_ROLES: BigInt(0x10000000),
MANAGE_WEBHOOKS: BigInt(0x20000000), // MANAGE_WEBHOOKS: BigInt(0x20000000),
MANAGE_EMOJIS: BigInt(0x40000000), // MANAGE_EMOJIS: BigInt(0x40000000),
}; };

View file

@ -65,9 +65,9 @@ export const Role = (props: Props) => {
const disabledReason = (role: RPCRole) => { const disabledReason = (role: RPCRole) => {
switch (role.safety) { switch (role.safety) {
case RoleSafety.HIGHERTHANBOT: case RoleSafety.HigherThanBot:
return `This role is above Roleypoly's own role.`; return `This role is above Roleypoly's own role.`;
case RoleSafety.DANGEROUSPERMISSIONS: case RoleSafety.DangerousPermissions:
const rolePermissions = BigInt(role.permissions); const rolePermissions = BigInt(role.permissions);
let permissionHits: string[] = []; let permissionHits: string[] = [];

View file

@ -19,7 +19,7 @@ type Props = {
}; };
const typeEnumToSwitch = (typeData: CategoryType) => { const typeEnumToSwitch = (typeData: CategoryType) => {
if (typeData === CategoryType.SINGLE) { if (typeData === CategoryType.Single) {
return 'Single'; return 'Single';
} else { } else {
return 'Multiple'; return 'Multiple';
@ -28,9 +28,9 @@ const typeEnumToSwitch = (typeData: CategoryType) => {
const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => { const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => {
if (typeData === 'Single') { if (typeData === 'Single') {
return CategoryType.SINGLE; return CategoryType.Single;
} else { } else {
return CategoryType.MULTI; return CategoryType.Multi;
} }
}; };
@ -53,14 +53,14 @@ export const EditorCategory = (props: Props) => {
updateSearchTerm(''); updateSearchTerm('');
props.onChange({ props.onChange({
...props.category, ...props.category,
rolesList: [...props.category.rolesList, role.id], roles: [...props.category.roles, role.id],
}); });
}; };
const handleRoleDeselect = (role: RoleType) => () => { const handleRoleDeselect = (role: RoleType) => () => {
props.onChange({ props.onChange({
...props.category, ...props.category,
rolesList: props.category.rolesList.filter((x) => x !== role.id), roles: props.category.roles.filter((x) => x !== role.id),
}); });
}; };
@ -122,7 +122,7 @@ export const EditorCategory = (props: Props) => {
onChange={(x) => updateSearchTerm(x.target.value)} onChange={(x) => updateSearchTerm(x.target.value)}
/> />
<RoleContainer> <RoleContainer>
{props.category.rolesList.map((id) => { {props.category.roles.map((id) => {
const role = props.guildRoles.find((x) => x.id === id); const role = props.guildRoles.find((x) => x.id === id);
if (!role) { if (!role) {
return <></>; return <></>;

View file

@ -56,7 +56,7 @@ export const PickerCategory = (props: CategoryProps) => (
role={role} role={role}
selected={props.selectedRoles.includes(role.id)} selected={props.selectedRoles.includes(role.id)}
onClick={props.onChange(role)} onClick={props.onChange(role)}
disabled={role.safety !== RoleSafety.SAFE} disabled={role.safety !== RoleSafety.Safe}
tooltipId={props.category.id} tooltipId={props.category.id}
/> />
</Container> </Container>

View file

@ -17,12 +17,12 @@ export const EditorShell = (props: Props) => (
const RolesTab = (props: Props) => ( const RolesTab = (props: Props) => (
<div> <div>
{props.guild.data.categoriesList.map((category, idx) => ( {props.guild.data.categories.map((category, idx) => (
<CategoryContainer key={idx}> <CategoryContainer key={idx}>
<EditorCategory <EditorCategory
category={category} category={category}
uncategorizedRoles={[]} uncategorizedRoles={[]}
guildRoles={props.guild.roles.rolesList} guildRoles={props.guild.roles}
onChange={(x) => console.log(x)} onChange={(x) => console.log(x)}
/> />
</CategoryContainer> </CategoryContainer>

View file

@ -8,9 +8,10 @@ import * as React from 'react';
import { import {
guild, guild,
guildData, guildData,
guildRoles,
member, member,
mockCategorySingle, mockCategorySingle,
roleCategory,
roleCategory2,
} from 'roleypoly/common/types/storyData'; } from 'roleypoly/common/types/storyData';
import { Role } from 'roleypoly/design-system/atoms/role'; import { Role } from 'roleypoly/design-system/atoms/role';
import { PickerCategory } from 'roleypoly/design-system/molecules/picker-category'; import { PickerCategory } from 'roleypoly/design-system/molecules/picker-category';
@ -19,9 +20,9 @@ import { RolePicker, RolePickerProps } from './RolePicker';
it('unselects the rest of a category in single mode', () => { it('unselects the rest of a category in single mode', () => {
const props: RolePickerProps = { const props: RolePickerProps = {
guildData: { ...guildData, categoriesList: [mockCategorySingle] }, guildData: { ...guildData, categories: [mockCategorySingle] },
member: { ...member, rolesList: [] }, member: { ...member, roles: [] },
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
guild: guild, guild: guild,
onSubmit: jest.fn(), onSubmit: jest.fn(),
editable: false, editable: false,
@ -34,10 +35,10 @@ it('unselects the rest of a category in single mode', () => {
roles.first().props().onClick?.(true); roles.first().props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit(); view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.rolesList[0]]); expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[0]]);
roles.last().props().onClick?.(true); roles.at(1).props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit(); view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.rolesList[1]]); expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[1]]);
}); });

View file

@ -1,12 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { guild, guildData, guildRoles, member } from 'roleypoly/common/types/storyData'; import {
guild,
guildData,
member,
roleCategory,
roleCategory2,
} from 'roleypoly/common/types/storyData';
import { RolePicker, RolePickerProps } from './RolePicker'; import { RolePicker, RolePickerProps } from './RolePicker';
const props: Partial<RolePickerProps> = { const props: Partial<RolePickerProps> = {
guildData: guildData, guildData: guildData,
member: member, member: member,
guild: guild, guild: guild,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
editable: false, editable: false,
}; };

View file

@ -5,7 +5,6 @@ import {
CategoryType, CategoryType,
Guild, Guild,
GuildData, GuildData,
GuildRoles,
Member, Member,
Role, Role,
} from 'roleypoly/common/types'; } from 'roleypoly/common/types';
@ -27,7 +26,7 @@ export type RolePickerProps = {
guild: Guild; guild: Guild;
guildData: GuildData; guildData: GuildData;
member: Member; member: Member;
roles: GuildRoles; roles: Role[];
onSubmit: (selectedRoles: string[]) => void; onSubmit: (selectedRoles: string[]) => void;
editable: boolean; editable: boolean;
}; };
@ -43,15 +42,15 @@ const arrayMatches = (a: any[], b: any[]) => {
export const RolePicker = (props: RolePickerProps) => { export const RolePicker = (props: RolePickerProps) => {
const [selectedRoles, updateSelectedRoles] = React.useState<string[]>( const [selectedRoles, updateSelectedRoles] = React.useState<string[]>(
props.member.rolesList props.member.roles
); );
const handleChange = (category: Category) => (role: Role) => (newState: boolean) => { const handleChange = (category: Category) => (role: Role) => (newState: boolean) => {
if (category.type === CategoryType.SINGLE) { if (category.type === CategoryType.Single) {
updateSelectedRoles( updateSelectedRoles(
newState === true newState === true
? [ ? [
...selectedRoles.filter((x) => !category.rolesList.includes(x)), ...selectedRoles.filter((x) => !category.roles.includes(x)),
role.id, role.id,
] ]
: selectedRoles.filter((x) => x !== role.id) : selectedRoles.filter((x) => x !== role.id)
@ -79,31 +78,29 @@ export const RolePicker = (props: RolePickerProps) => {
</> </>
)} )}
{props.guildData.categoriesList.length !== 0 ? ( {props.guildData.categories.length !== 0 ? (
<> <>
<div> <div>
{props.guildData.categoriesList.map((category, idx) => ( {props.guildData.categories.map((category, idx) => (
<CategoryContainer key={idx}> <CategoryContainer key={idx}>
<PickerCategory <PickerCategory
key={idx} key={idx}
category={category} category={category}
title={category.name} title={category.name}
selectedRoles={selectedRoles.filter((roleId) => selectedRoles={selectedRoles.filter((roleId) =>
category.rolesList.includes(roleId) category.roles.includes(roleId)
)} )}
roles={ roles={
category.rolesList category.roles
.map((role) => .map((role) =>
props.roles.rolesList.find( props.roles.find((r) => r.id === role)
(r) => r.id === role
)
) )
.filter((r) => r !== undefined) as Role[] .filter((r) => r !== undefined) as Role[]
} }
onChange={handleChange(category)} onChange={handleChange(category)}
wikiMode={false} wikiMode={false}
type={ type={
category.type === CategoryType.SINGLE category.type === CategoryType.Single
? 'single' ? 'single'
: 'multi' : 'multi'
} }
@ -112,12 +109,12 @@ export const RolePicker = (props: RolePickerProps) => {
))} ))}
</div> </div>
<FaderOpacity <FaderOpacity
isVisible={!arrayMatches(selectedRoles, props.member.rolesList)} isVisible={!arrayMatches(selectedRoles, props.member.roles)}
> >
<ResetSubmit <ResetSubmit
onSubmit={() => props.onSubmit(selectedRoles)} onSubmit={() => props.onSubmit(selectedRoles)}
onReset={() => { onReset={() => {
updateSelectedRoles(props.member.rolesList); updateSelectedRoles(props.member.roles);
}} }}
/> />
</FaderOpacity> </FaderOpacity>

View file

@ -3,9 +3,10 @@ import {
guild, guild,
guildData, guildData,
guildEnum, guildEnum,
guildRoles,
mastheadSlugs, mastheadSlugs,
member, member,
roleCategory,
roleCategory2,
user, user,
} from 'roleypoly/common/types/storyData'; } from 'roleypoly/common/types/storyData';
import { RolePickerTemplate, RolePickerTemplateProps } from './RolePicker'; import { RolePickerTemplate, RolePickerTemplateProps } from './RolePicker';
@ -19,7 +20,7 @@ const props: RolePickerTemplateProps = {
member: member, member: member,
guild: guild, guild: guild,
guilds: mastheadSlugs, guilds: mastheadSlugs,
roles: guildRoles, roles: [...roleCategory, ...roleCategory2],
editable: false, editable: false,
user: user, user: user,
guildEnumeration: guildEnum, guildEnumeration: guildEnum,