chore: update prettier tab width for consistency (#175)

This commit is contained in:
41666 2021-03-13 22:54:34 -05:00 committed by GitHub
parent a931f8c69c
commit f24d2fcc99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
247 changed files with 7224 additions and 7375 deletions

View file

@ -4,33 +4,33 @@ import { botClientID } from '../utils/config';
const validGuildID = /^[0-9]+$/;
type URLParams = {
clientID: string;
permissions: number;
guildID?: string;
clientID: string;
permissions: number;
guildID?: string;
};
const buildURL = (params: URLParams) => {
let url = `https://discord.com/api/oauth2/authorize?client_id=${params.clientID}&scope=bot&permissions=${params.permissions}`;
let url = `https://discord.com/api/oauth2/authorize?client_id=${params.clientID}&scope=bot&permissions=${params.permissions}`;
if (params.guildID) {
url += `&guild_id=${params.guildID}&disable_guild_select=true`;
}
if (params.guildID) {
url += `&guild_id=${params.guildID}&disable_guild_select=true`;
}
return url;
return url;
};
export const BotJoin = (request: Request): Response => {
let guildID = new URL(request.url).searchParams.get('guild') || '';
let guildID = new URL(request.url).searchParams.get('guild') || '';
if (guildID && !validGuildID.test(guildID)) {
guildID = '';
}
if (guildID && !validGuildID.test(guildID)) {
guildID = '';
}
return Bounce(
buildURL({
clientID: botClientID,
permissions: 268435456,
guildID,
})
);
return Bounce(
buildURL({
clientID: botClientID,
permissions: 268435456,
guildID,
})
);
};

View file

@ -5,90 +5,78 @@ import { GuildData } from '../utils/kv';
// Temporary use.
export const CreateRoleypolyData = onlyRootUsers(
async (request: Request): Promise<Response> => {
const data: GuildDataT = {
id: '386659935687147521',
message:
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
features: Features.Preview,
categories: [
{
id: KSUID.randomSync().string,
name: 'Demo Roles',
type: CategoryType.Multi,
hidden: false,
position: 0,
roles: [
'557825026406088717',
'557824994269200384',
'557824893241131029',
'557812915386843170',
'557812901717737472',
'557812805546541066',
],
},
{
id: KSUID.randomSync().string,
name: 'Colors',
type: CategoryType.Single,
hidden: false,
position: 1,
roles: [
'394060232893923349',
'394060145799331851',
'394060192846839809',
],
},
{
id: KSUID.randomSync().string,
name: 'Test Roles',
type: CategoryType.Multi,
hidden: false,
position: 5,
roles: [
'558104828216213505',
'558103534453653514',
'558297233582194728',
],
},
{
id: KSUID.randomSync().string,
name: 'Region',
type: CategoryType.Multi,
hidden: false,
position: 3,
roles: [
'397296181803483136',
'397296137066774529',
'397296218809827329',
'397296267283267605',
],
},
{
id: KSUID.randomSync().string,
name: 'Opt-in Channels',
type: CategoryType.Multi,
hidden: false,
position: 4,
roles: ['414514823959674890', '764230661904007219'],
},
{
id: KSUID.randomSync().string,
name: 'Pronouns',
type: CategoryType.Multi,
hidden: false,
position: 2,
roles: [
'485916566790340608',
'485916566941335583',
'485916566311927808',
],
},
],
};
async (request: Request): Promise<Response> => {
const data: GuildDataT = {
id: '386659935687147521',
message:
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
features: Features.Preview,
categories: [
{
id: KSUID.randomSync().string,
name: 'Demo Roles',
type: CategoryType.Multi,
hidden: false,
position: 0,
roles: [
'557825026406088717',
'557824994269200384',
'557824893241131029',
'557812915386843170',
'557812901717737472',
'557812805546541066',
],
},
{
id: KSUID.randomSync().string,
name: 'Colors',
type: CategoryType.Single,
hidden: false,
position: 1,
roles: ['394060232893923349', '394060145799331851', '394060192846839809'],
},
{
id: KSUID.randomSync().string,
name: 'Test Roles',
type: CategoryType.Multi,
hidden: false,
position: 5,
roles: ['558104828216213505', '558103534453653514', '558297233582194728'],
},
{
id: KSUID.randomSync().string,
name: 'Region',
type: CategoryType.Multi,
hidden: false,
position: 3,
roles: [
'397296181803483136',
'397296137066774529',
'397296218809827329',
'397296267283267605',
],
},
{
id: KSUID.randomSync().string,
name: 'Opt-in Channels',
type: CategoryType.Multi,
hidden: false,
position: 4,
roles: ['414514823959674890', '764230661904007219'],
},
{
id: KSUID.randomSync().string,
name: 'Pronouns',
type: CategoryType.Multi,
hidden: false,
position: 2,
roles: ['485916566790340608', '485916566941335583', '485916566311927808'],
},
],
};
await GuildData.put(data.id, data);
await GuildData.put(data.id, data);
return respond({ ok: true });
}
return respond({ ok: true });
}
);

View file

@ -5,52 +5,52 @@ 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('/');
(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);
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);
}
);

View file

@ -2,11 +2,11 @@ import { SessionData } from '@roleypoly/types';
import { respond, withSession } from '../utils/api-tools';
export const GetSession = withSession((session?: SessionData) => (): Response => {
const { user, guilds, sessionID } = session || {};
const { user, guilds, sessionID } = session || {};
return respond({
user,
guilds,
sessionID,
});
return respond({
user,
guilds,
sessionID,
});
});

View file

@ -3,38 +3,38 @@ import { respond } from '../utils/api-tools';
import { getGuild } from '../utils/guild';
export const GetSlug = async (request: Request): Promise<Response> => {
const reqURL = new URL(request.url);
const [, , serverID] = reqURL.pathname.split('/');
const reqURL = new URL(request.url);
const [, , serverID] = reqURL.pathname.split('/');
if (!serverID) {
return respond(
{
error: 'missing server ID',
},
{
status: 400,
}
);
}
if (!serverID) {
return respond(
{
error: 'missing server ID',
},
{
status: 400,
}
);
}
const guild = await getGuild(serverID);
if (!guild) {
return respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
}
const guild = await getGuild(serverID);
if (!guild) {
return respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
}
const { id, name, icon } = guild;
const guildSlug: GuildSlug = {
id,
name,
icon,
permissionLevel: 0,
};
return respond(guildSlug);
const { id, name, icon } = guild;
const guildSlug: GuildSlug = {
id,
name,
icon,
permissionLevel: 0,
};
return respond(guildSlug);
};

View file

@ -4,30 +4,30 @@ import { Bounce } from '../utils/bounce';
import { apiPublicURI, botClientID } from '../utils/config';
type URLParams = {
clientID: string;
redirectURI: string;
state: string;
clientID: string;
redirectURI: string;
state: string;
};
const buildURL = (params: URLParams) =>
`https://discord.com/api/oauth2/authorize?client_id=${
params.clientID
}&response_type=code&scope=identify%20guilds&redirect_uri=${encodeURIComponent(
params.redirectURI
)}&state=${params.state}`;
`https://discord.com/api/oauth2/authorize?client_id=${
params.clientID
}&response_type=code&scope=identify%20guilds&redirect_uri=${encodeURIComponent(
params.redirectURI
)}&state=${params.state}`;
export const LoginBounce = async (request: Request): Promise<Response> => {
const stateSessionData: StateSession = {};
const stateSessionData: StateSession = {};
const { cbh: callbackHost } = getQuery(request);
if (callbackHost && isAllowedCallbackHost(callbackHost)) {
stateSessionData.callbackHost = callbackHost;
}
const { cbh: callbackHost } = getQuery(request);
if (callbackHost && isAllowedCallbackHost(callbackHost)) {
stateSessionData.callbackHost = callbackHost;
}
const state = await setupStateSession(stateSessionData);
const state = await setupStateSession(stateSessionData);
const redirectURI = `${apiPublicURI}/login-callback`;
const clientID = botClientID;
const redirectURI = `${apiPublicURI}/login-callback`;
const clientID = botClientID;
return Bounce(buildURL({ state, redirectURI, clientID }));
return Bounce(buildURL({ state, redirectURI, clientID }));
};

View file

@ -1,159 +1,159 @@
import {
AuthTokenResponse,
DiscordUser,
GuildSlug,
SessionData,
StateSession,
AuthTokenResponse,
DiscordUser,
GuildSlug,
SessionData,
StateSession,
} from '@roleypoly/types';
import KSUID from 'ksuid';
import {
AuthType,
discordFetch,
formData,
getStateSession,
isAllowedCallbackHost,
parsePermissions,
resolveFailures,
userAgent,
AuthType,
discordFetch,
formData,
getStateSession,
isAllowedCallbackHost,
parsePermissions,
resolveFailures,
userAgent,
} from '../utils/api-tools';
import { Bounce } from '../utils/bounce';
import { apiPublicURI, botClientID, botClientSecret, uiPublicURI } from '../utils/config';
import { Sessions } from '../utils/kv';
const AuthErrorResponse = (extra?: string) =>
Bounce(
uiPublicURI +
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
);
Bounce(
uiPublicURI +
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
);
export const LoginCallback = resolveFailures(
AuthErrorResponse,
async (request: Request): Promise<Response> => {
let bounceBaseUrl = uiPublicURI;
AuthErrorResponse,
async (request: Request): Promise<Response> => {
let bounceBaseUrl = uiPublicURI;
const query = new URL(request.url).searchParams;
const stateValue = query.get('state');
const query = new URL(request.url).searchParams;
const stateValue = query.get('state');
if (stateValue === null) {
return AuthErrorResponse('state missing');
}
try {
const state = KSUID.parse(stateValue);
const stateExpiry = state.date.getTime() + 1000 * 60 * 5;
const currentTime = Date.now();
if (currentTime > stateExpiry) {
return AuthErrorResponse('state expired');
}
const stateSession = await getStateSession<StateSession>(state.string);
if (
stateSession?.callbackHost &&
isAllowedCallbackHost(stateSession.callbackHost)
) {
bounceBaseUrl = stateSession.callbackHost;
}
} catch (e) {
return AuthErrorResponse('state invalid');
}
const code = query.get('code');
if (!code) {
return AuthErrorResponse('code missing');
}
const tokenRequest = {
client_id: botClientID,
client_secret: botClientSecret,
grant_type: 'authorization_code',
redirect_uri: apiPublicURI + '/login-callback',
scope: 'identify guilds',
code,
};
const tokenFetch = await fetch('https://discord.com/api/v8/oauth2/token', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'user-agent': userAgent,
},
body: formData(tokenRequest),
});
const tokens = (await tokenFetch.json()) as AuthTokenResponse;
if (!tokens.access_token) {
return AuthErrorResponse('token response invalid');
}
const [sessionID, user, guilds] = await Promise.all([
KSUID.random(),
getUser(tokens.access_token),
getGuilds(tokens.access_token),
]);
if (!user) {
return AuthErrorResponse('failed to fetch user');
}
const sessionData: SessionData = {
tokens,
sessionID: sessionID.string,
user,
guilds,
};
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
return Bounce(
bounceBaseUrl + '/machinery/new-session?session_id=' + sessionID.string
);
if (stateValue === null) {
return AuthErrorResponse('state missing');
}
try {
const state = KSUID.parse(stateValue);
const stateExpiry = state.date.getTime() + 1000 * 60 * 5;
const currentTime = Date.now();
if (currentTime > stateExpiry) {
return AuthErrorResponse('state expired');
}
const stateSession = await getStateSession<StateSession>(state.string);
if (
stateSession?.callbackHost &&
isAllowedCallbackHost(stateSession.callbackHost)
) {
bounceBaseUrl = stateSession.callbackHost;
}
} catch (e) {
return AuthErrorResponse('state invalid');
}
const code = query.get('code');
if (!code) {
return AuthErrorResponse('code missing');
}
const tokenRequest = {
client_id: botClientID,
client_secret: botClientSecret,
grant_type: 'authorization_code',
redirect_uri: apiPublicURI + '/login-callback',
scope: 'identify guilds',
code,
};
const tokenFetch = await fetch('https://discord.com/api/v8/oauth2/token', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'user-agent': userAgent,
},
body: formData(tokenRequest),
});
const tokens = (await tokenFetch.json()) as AuthTokenResponse;
if (!tokens.access_token) {
return AuthErrorResponse('token response invalid');
}
const [sessionID, user, guilds] = await Promise.all([
KSUID.random(),
getUser(tokens.access_token),
getGuilds(tokens.access_token),
]);
if (!user) {
return AuthErrorResponse('failed to fetch user');
}
const sessionData: SessionData = {
tokens,
sessionID: sessionID.string,
user,
guilds,
};
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
return Bounce(
bounceBaseUrl + '/machinery/new-session?session_id=' + sessionID.string
);
}
);
const getUser = async (accessToken: string): Promise<DiscordUser | null> => {
const user = await discordFetch<DiscordUser>(
'/users/@me',
accessToken,
AuthType.Bearer
);
const user = await discordFetch<DiscordUser>(
'/users/@me',
accessToken,
AuthType.Bearer
);
if (!user) {
return null;
}
if (!user) {
return null;
}
const { id, username, discriminator, bot, avatar } = user;
const { id, username, discriminator, bot, avatar } = user;
return { id, username, discriminator, bot, avatar };
return { id, username, discriminator, bot, avatar };
};
type UserGuildsPayload = {
id: string;
name: string;
icon: string;
owner: boolean;
permissions: number;
features: string[];
id: string;
name: string;
icon: string;
owner: boolean;
permissions: number;
features: string[];
}[];
const getGuilds = async (accessToken: string) => {
const guilds = await discordFetch<UserGuildsPayload>(
'/users/@me/guilds',
accessToken,
AuthType.Bearer
);
const guilds = await discordFetch<UserGuildsPayload>(
'/users/@me/guilds',
accessToken,
AuthType.Bearer
);
if (!guilds) {
return [];
}
if (!guilds) {
return [];
}
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
id: guild.id,
name: guild.name,
icon: guild.icon,
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
}));
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
id: guild.id,
name: guild.name,
icon: guild.icon,
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
}));
return guildSlugs;
return guildSlugs;
};

View file

@ -4,24 +4,24 @@ import { botClientID, botClientSecret } from '../utils/config';
import { Sessions } from '../utils/kv';
export const RevokeSession = withSession(
(session: SessionData) => async (request: Request) => {
const tokenRequest = {
token: session.tokens.access_token,
client_id: botClientID,
client_secret: botClientSecret,
};
(session: SessionData) => async (request: Request) => {
const tokenRequest = {
token: session.tokens.access_token,
client_id: botClientID,
client_secret: botClientSecret,
};
await fetch('https://discord.com/api/v8/oauth2/token/revoke', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'user-agent': userAgent,
},
body: formData(tokenRequest),
});
await fetch('https://discord.com/api/v8/oauth2/token/revoke', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'user-agent': userAgent,
},
body: formData(tokenRequest),
});
await Sessions.delete(session.sessionID);
await Sessions.delete(session.sessionID);
return respond({ ok: true });
}
return respond({ ok: true });
}
);

View file

@ -1,141 +1,135 @@
import {
GuildData,
Member,
Role,
RoleSafety,
RoleTransaction,
RoleUpdate,
SessionData,
TransactionType,
GuildData,
Member,
Role,
RoleSafety,
RoleTransaction,
RoleUpdate,
SessionData,
TransactionType,
} from '@roleypoly/types';
import { difference, groupBy, keyBy, union } from 'lodash';
import { AuthType, discordFetch, respond, withSession } from '../utils/api-tools';
import { botToken } from '../utils/config';
import {
getGuild,
getGuildData,
getGuildMemberRoles,
updateGuildMemberRoles,
getGuild,
getGuildData,
getGuildMemberRoles,
updateGuildMemberRoles,
} from '../utils/guild';
const notFound = () => respond({ error: 'guild not found' }, { status: 404 });
export const UpdateRoles = withSession(
({ guilds, user: { id: userID } }: SessionData) => async (request: Request) => {
const updateRequest = (await request.json()) as RoleUpdate;
const [, , guildID] = new URL(request.url).pathname.split('/');
({ guilds, user: { id: userID } }: SessionData) => async (request: Request) => {
const updateRequest = (await request.json()) as RoleUpdate;
const [, , guildID] = new URL(request.url).pathname.split('/');
if (!guildID) {
return respond({ error: 'guild ID missing from URL' }, { status: 400 });
}
if (updateRequest.transactions.length === 0) {
return respond(
{ error: 'must have as least one transaction' },
{ status: 400 }
);
}
const guildCheck = guilds.find((guild) => guild.id === guildID);
if (!guildCheck) {
return notFound();
}
const guild = await getGuild(guildID);
if (!guild) {
return notFound();
}
const guildMemberRoles = await getGuildMemberRoles(
{ serverID: guildID, userID },
{ skipCachePull: true }
);
if (!guildMemberRoles) {
return notFound();
}
const newRoles = calculateNewRoles({
currentRoles: guildMemberRoles,
guildRoles: guild.roles,
guildData: await getGuildData(guildID),
updateRequest,
});
const patchMemberRoles = await discordFetch<Member>(
`/guilds/${guildID}/members/${userID}`,
botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
roles: newRoles,
}),
}
);
if (!patchMemberRoles) {
return respond({ error: 'discord rejected the request' }, { status: 500 });
}
const updatedMember: Member = {
roles: patchMemberRoles.roles,
};
await updateGuildMemberRoles(
{ serverID: guildID, userID },
patchMemberRoles.roles
);
return respond(updatedMember);
if (!guildID) {
return respond({ error: 'guild ID missing from URL' }, { status: 400 });
}
if (updateRequest.transactions.length === 0) {
return respond({ error: 'must have as least one transaction' }, { status: 400 });
}
const guildCheck = guilds.find((guild) => guild.id === guildID);
if (!guildCheck) {
return notFound();
}
const guild = await getGuild(guildID);
if (!guild) {
return notFound();
}
const guildMemberRoles = await getGuildMemberRoles(
{ serverID: guildID, userID },
{ skipCachePull: true }
);
if (!guildMemberRoles) {
return notFound();
}
const newRoles = calculateNewRoles({
currentRoles: guildMemberRoles,
guildRoles: guild.roles,
guildData: await getGuildData(guildID),
updateRequest,
});
const patchMemberRoles = await discordFetch<Member>(
`/guilds/${guildID}/members/${userID}`,
botToken,
AuthType.Bot,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
roles: newRoles,
}),
}
);
if (!patchMemberRoles) {
return respond({ error: 'discord rejected the request' }, { status: 500 });
}
const updatedMember: Member = {
roles: patchMemberRoles.roles,
};
await updateGuildMemberRoles({ serverID: guildID, userID }, patchMemberRoles.roles);
return respond(updatedMember);
}
);
const calculateNewRoles = ({
currentRoles,
guildData,
guildRoles,
updateRequest,
currentRoles,
guildData,
guildRoles,
updateRequest,
}: {
currentRoles: string[];
guildRoles: Role[];
guildData: GuildData;
updateRequest: RoleUpdate;
currentRoles: string[];
guildRoles: Role[];
guildData: GuildData;
updateRequest: RoleUpdate;
}): string[] => {
const roleMap = keyBy(guildRoles, 'id');
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);
// 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,
[]
);
// 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 safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) =>
allSafeRoles.includes(tx.id)
);
const changesByAction = groupBy(safeTransactions, 'action');
const changesByAction = groupBy(safeTransactions, 'action');
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map((tx) => tx.id);
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
(tx) => tx.id
);
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map((tx) => tx.id);
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
(tx) => tx.id
);
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
return final;
return final;
};