From 64b38348d43040aba9823003b28acbb906e8a7d7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 14 May 2023 21:33:04 -0300 Subject: [PATCH] feat(plugins): Permissions Viewer (#477) Co-authored-by: V --- README.md | 2 +- src/components/Icons.tsx | 40 +++- src/components/iconStyles.css | 4 + .../components/RolesAndUsersPermissions.tsx | 225 ++++++++++++++++++ .../components/UserPermissions.tsx | 175 ++++++++++++++ .../permissionsViewer/components/icons.tsx | 58 +++++ src/plugins/permissionsViewer/index.tsx | 180 ++++++++++++++ src/plugins/permissionsViewer/styles.css | 128 ++++++++++ src/plugins/permissionsViewer/utils.ts | 84 +++++++ src/webpack/common/types/utils.d.ts | 45 ++++ src/webpack/common/utils.ts | 2 + 11 files changed, 938 insertions(+), 5 deletions(-) create mode 100644 src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx create mode 100644 src/plugins/permissionsViewer/components/UserPermissions.tsx create mode 100644 src/plugins/permissionsViewer/components/icons.tsx create mode 100644 src/plugins/permissionsViewer/index.tsx create mode 100644 src/plugins/permissionsViewer/styles.css create mode 100644 src/plugins/permissionsViewer/utils.ts diff --git a/README.md b/README.md index 94434739..771495d2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The cutest Discord client mod - Super easy to install (Download Installer, open, click install button, done) - 100+ plugins built in: [See a list](https://vencord.dev/plugins) - - Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB + - Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB - Fairly lightweight despite the many inbuilt plugins - Excellent Browser Support: Run Vencord in your Browser via extension or UserScript - Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!) diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index d1cc7a67..4227fcea 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -19,27 +19,28 @@ import "./iconStyles.css"; import { classes } from "@utils/misc"; -import type { PropsWithChildren } from "react"; +import { i18n } from "@webpack/common"; +import type { PropsWithChildren, SVGProps } from "react"; interface BaseIconProps extends IconProps { viewBox: string; } -interface IconProps { +interface IconProps extends SVGProps { className?: string; height?: number; width?: number; } -function Icon({ height = 24, width = 24, className, children, viewBox }: PropsWithChildren) { +function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren) { return ( @@ -114,3 +115,34 @@ export function ImageIcon(props: IconProps) { ); } + +export function InfoIcon(props: IconProps) { + return ( + + + + ); +} + +export function OwnerCrownIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/iconStyles.css b/src/components/iconStyles.css index 9f2ef8e8..ca4075da 100644 --- a/src/components/iconStyles.css +++ b/src/components/iconStyles.css @@ -1,3 +1,7 @@ .vc-open-external-icon { transform: rotate(45deg); } + +.vc-owner-crown-icon { + color: var(--text-warning); +} diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx new file mode 100644 index 00000000..7a65a071 --- /dev/null +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -0,0 +1,225 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; +import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { ContextMenu, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; +import type { Guild } from "discord-types/general"; + +import { cl, getPermissionDescription, getPermissionString } from "../utils"; +import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons"; + +export const enum PermissionType { + Role = 0, + User = 1, + Owner = 2 +} + +export interface RoleOrUserPermission { + type: PermissionType; + id?: string; + permissions?: bigint; + overwriteAllow?: bigint; + overwriteDeny?: bigint; +} + +function openRolesAndUsersPermissionsModal(permissions: Array, guild: Guild, header: string) { + return openModal(modalProps => ( + + )); +} + +function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array; guild: Guild; modalProps: ModalProps; header: string; }) { + permissions.sort((a, b) => a.type - b.type); + + useStateFromStores( + [GuildMemberStore], + () => GuildMemberStore.getMemberIds(guild.id), + null, + (old, current) => old.length === current.length + ); + + useEffect(() => { + const usersToRequest = permissions + .filter(p => p.type === PermissionType.User && !GuildMemberStore.isMember(guild.id, p.id!)) + .map(({ id }) => id); + + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildIds: [guild.id], + userIds: usersToRequest + }); + }, []); + + const [selectedItemIndex, selectItem] = useState(0); + const selectedItem = permissions[selectedItemIndex]; + + return ( + + + {header} permissions: + + + + + {!selectedItem && ( +
+ No permissions to display! +
+ )} + + {selectedItem && ( +
+
+ {permissions.map((permission, index) => { + const user = UserStore.getUser(permission.id ?? ""); + const role = guild.roles[permission.id ?? ""]; + + return ( + + ); + })} +
+
+ {Object.entries(PermissionsBits).map(([permissionName, bit]) => ( +
+
+ {(() => { + const { permissions, overwriteAllow, overwriteDeny } = selectedItem; + + if (permissions) + return (permissions & bit) === bit + ? PermissionAllowedIcon() + : PermissionDeniedIcon(); + + if (overwriteAllow && (overwriteAllow & bit) === bit) + return PermissionAllowedIcon(); + if (overwriteDeny && (overwriteDeny & bit) === bit) + return PermissionDeniedIcon(); + + return PermissionDefaultIcon(); + })()} +
+ {getPermissionString(permissionName)} + + + {props => } + +
+ ))} +
+
+ )} +
+
+ ); +} + +function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: string; onClose: () => void; }) { + return ( + + { + const role = guild.roles[roleId]; + if (!role) return; + + onClose(); + + FluxDispatcher.dispatch({ + type: "IMPERSONATE_UPDATE", + guildId: guild.id, + data: { + type: "ROLES", + roles: { + [roleId]: role + } + } + }); + }} + /> + + ); +} + +const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent); + +export default openRolesAndUsersPermissionsModal; diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx new file mode 100644 index 00000000..acffa787 --- /dev/null +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -0,0 +1,175 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { proxyLazy } from "@utils/lazy"; +import { classes } from "@utils/misc"; +import { filters, findBulk } from "@webpack"; +import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore, useState } from "@webpack/common"; +import type { Guild, GuildMember } from "discord-types/general"; + +import { settings } from ".."; +import { cl, getPermissionString, getSortedRoles, sortUserRoles } from "../utils"; +import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./RolesAndUsersPermissions"; + +interface UserPermission { + permission: string; + roleColor: string; + rolePosition: number; +} + +type UserPermissions = Array; + +const Classes = proxyLazy(() => { + const modules = findBulk( + filters.byProps("roles", "rolePill", "rolePillBorder"), + filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"), + filters.byProps("roleNameOverflow", "root", "roleName", "roleRemoveButton") + ); + + return Object.assign({}, ...modules); +}) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; + +function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) { + const [viewPermissions, setViewPermissions] = useState(settings.store.defaultPermissionsDropdownState); + + const [rolePermissions, userPermissions] = useMemo(() => { + const userPermissions: UserPermissions = []; + + const userRoles = getSortedRoles(guild, guildMember); + + const rolePermissions: Array = userRoles.map(role => ({ + type: PermissionType.Role, + ...role + })); + + if (guild.ownerId === guildMember.userId) { + rolePermissions.push({ + type: PermissionType.Owner, + permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n) + }); + + const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner"; + userPermissions.push({ + permission: OWNER, + roleColor: "var(--primary-300)", + rolePosition: Infinity + }); + } + + sortUserRoles(userRoles); + + for (const [permission, bit] of Object.entries(PermissionsBits)) { + for (const { permissions, colorString, position, name } of userRoles) { + if ((permissions & bit) === bit) { + userPermissions.push({ + permission: getPermissionString(permission), + roleColor: colorString || "var(--primary-300)", + rolePosition: position + }); + + break; + } + } + } + + userPermissions.sort((a, b) => b.rolePosition - a.rolePosition); + + return [rolePermissions, userPermissions]; + }, []); + + const { root, role, roleRemoveButton, roleNameOverflow, roles, rolePill, rolePillBorder, roleCircle, roleName } = Classes; + + return ( +
+
+ Permissions + +
+ + {tooltipProps => ( + + )} + + + + {tooltipProps => ( + + )} + +
+
+ + {viewPermissions && userPermissions.length > 0 && ( +
+ {userPermissions.map(({ permission, roleColor }) => ( +
+
+ +
+
+ + {permission} + +
+
+ ))} +
+ )} +
+ ); +} + +export default ErrorBoundary.wrap(UserPermissionsComponent, { noop: true }); diff --git a/src/plugins/permissionsViewer/components/icons.tsx b/src/plugins/permissionsViewer/components/icons.tsx new file mode 100644 index 00000000..8b58a440 --- /dev/null +++ b/src/plugins/permissionsViewer/components/icons.tsx @@ -0,0 +1,58 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export function PermissionDeniedIcon() { + return ( + + Denied + + + ); +} + +export function PermissionAllowedIcon() { + return ( + + Allowed + + + ); +} + +export function PermissionDefaultIcon() { + return ( + + + Not overwritten + + + + ); +} diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx new file mode 100644 index 00000000..793105a8 --- /dev/null +++ b/src/plugins/permissionsViewer/index.tsx @@ -0,0 +1,180 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import "./styles.css"; + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, GuildMemberStore, GuildStore, Menu, PermissionsBits, UserStore } from "@webpack/common"; +import type { Guild, GuildMember } from "discord-types/general"; + +import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; +import UserPermissions from "./components/UserPermissions"; +import { getSortedRoles } from "./utils"; + +export const enum PermissionsSortOrder { + HighestRole, + LowestRole +} + +const enum MenuItemParentType { + User, + Channel, + Guild +} + +export const settings = definePluginSettings({ + permissionsSortOrder: { + description: "The sort method used for defining which role grants an user a certain permission", + type: OptionType.SELECT, + options: [ + { label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true }, + { label: "Lowest Role", value: PermissionsSortOrder.LowestRole } + ], + }, + defaultPermissionsDropdownState: { + description: "Whether the permissions dropdown on user popouts should be open by default", + type: OptionType.BOOLEAN, + default: false, + } +}); + +function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { + return ( + { + const guild = GuildStore.getGuild(guildId); + + let permissions: RoleOrUserPermission[]; + let header: string; + + switch (type) { + case MenuItemParentType.User: { + const member = GuildMemberStore.getMember(guildId, id!); + + permissions = getSortedRoles(guild, member) + .map(role => ({ + type: PermissionType.Role, + ...role + })); + + if (guild.ownerId === id) { + permissions.push({ + type: PermissionType.Owner, + permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n) + }); + } + + header = member.nick ?? UserStore.getUser(member.userId).username; + + break; + } + + case MenuItemParentType.Channel: { + const channel = ChannelStore.getChannel(id!); + + permissions = Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ + type: type as PermissionType, + id, + overwriteAllow: allow, + overwriteDeny: deny + })); + + header = channel.name; + + break; + } + + default: { + permissions = Object.values(guild.roles).map(role => ({ + type: PermissionType.Role, + ...role + })); + + header = guild.name; + + break; + } + } + + openRolesAndUsersPermissionsModal(permissions, guild, header); + }} + /> + ); +} + +function makeContextMenuPatch(childId: string, type?: MenuItemParentType): NavContextMenuPatchCallback { + return (children, props) => () => { + if (!props) return children; + + const group = findGroupChildrenByChildId(childId, children); + + if (group) { + switch (type) { + case MenuItemParentType.User: + group.push(MenuItem(props.guildId, props.user.id, type)); + break; + case MenuItemParentType.Channel: + group.push(MenuItem(props.guild.id, props.channel.id, type)); + break; + case MenuItemParentType.Guild: + group.push(MenuItem(props.guild.id)); + break; + } + } + }; +} + +export default definePlugin({ + name: "PermissionsViewer", + description: "View the permissions a user or channel has, and the roles of a server", + authors: [Devs.Nuckyz, Devs.Ven], + settings, + + patches: [ + { + find: ".Messages.BOT_PROFILE_SLASH_COMMANDS", + replacement: { + match: /showBorder:.{0,60}}\),(?<=guild:(\i),guildMember:(\i),.+?)/, + replace: (m, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember}),` + } + } + ], + + UserPermissions: (guild: Guild, guildMember: GuildMember) => , + + userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User), + channelContextMenuPatch: makeContextMenuPatch("mute-channel", MenuItemParentType.Channel), + guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild), + + start() { + addContextMenuPatch("user-context", this.userContextMenuPatch); + addContextMenuPatch("channel-context", this.channelContextMenuPatch); + addContextMenuPatch("guild-context", this.guildContextMenuPatch); + }, + + stop() { + removeContextMenuPatch("user-context", this.userContextMenuPatch); + removeContextMenuPatch("channel-context", this.channelContextMenuPatch); + removeContextMenuPatch("guild-context", this.guildContextMenuPatch); + }, +}); diff --git a/src/plugins/permissionsViewer/styles.css b/src/plugins/permissionsViewer/styles.css new file mode 100644 index 00000000..6d6c1379 --- /dev/null +++ b/src/plugins/permissionsViewer/styles.css @@ -0,0 +1,128 @@ +/* User Permissions Component */ + +.vc-permviewer-userperms-title-container { + display: flex; + justify-content: space-between; +} + +.vc-permviewer-userperms-title { + margin-top: 10px; + margin-bottom: 6px; +} + +.vc-permviewer-userperms-permdetails-btn { + all: unset; + cursor: pointer; +} + +.vc-permviewer-userperms-toggleperms-btn { + all: unset; + cursor: pointer; +} + +/* RolesAndUsersPermissions Component */ + +.vc-permviewer-perms-title { + flex-grow: 1; +} + +.vc-permviewer-perms-no-perms { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.vc-permviewer-perms-container { + display: grid; + grid-template-columns: 1fr 2fr; + grid-template-areas: "list permissions"; + padding: 16px 0; +} + +.vc-permviewer-perms-list { + grid-area: list; + display: flex; + flex-direction: column; + border-right: 2px solid var(--background-modifier-active); +} + +.vc-permviewer-perms-list-item-btn { + all: unset; + cursor: pointer; +} + +.vc-permviewer-perms-list-item { + display: flex; + align-items: center; + padding: 8px 5px; + cursor: pointer; + width: 165px; +} + +.vc-permviewer-perms-list-item > div { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.vc-permviewer-perms-list-item-active { + background-color: var(--background-modifier-selected); + border-radius: 5px; +} + +.vc-permviewer-perms-role-circle { + border-radius: 50%; + width: 12px; + height: 12px; + margin-left: 3px; + margin-right: 11px; + flex-shrink: 0; +} + +.vc-permviewer-perms-user-img { + border-radius: 50%; + width: 20px; + height: 20px; + margin-right: 6px; +} + +.vc-permviewer-perms-perms { + grid-area: permissions; + display: flex; + flex-direction: column; + margin-left: 5px; +} + +.vc-permviewer-perms-perms-item { + position: relative; + display: flex; + align-items: center; + padding: 10px; + border-bottom: 2px solid var(--background-modifier-active); +} + +.vc-permviewer-perms-perms-item:last-child { + border: 0; +} + +.vc-permviewer-perms-perms-item-icon { + border: 1px solid var(--background-modifier-selected); + width: 25px; + height: 25px; + margin-right: 5px; +} + +.vc-permviewer-perms-perms-item .vc-info-icon { + color: var(--interactive-muted); + cursor: pointer; + position: absolute; + right: 0; + scale: 0.9; +} + +.vc-permviewer-perms-perms-item .vc-info-icon:hover { + color: var(--interactive-active); +} diff --git a/src/plugins/permissionsViewer/utils.ts b/src/plugins/permissionsViewer/utils.ts new file mode 100644 index 00000000..b7471479 --- /dev/null +++ b/src/plugins/permissionsViewer/utils.ts @@ -0,0 +1,84 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { classNameFactory } from "@api/Styles"; +import { wordsToTitle } from "@utils/text"; +import { i18n, Parser } from "@webpack/common"; +import { Guild, GuildMember, Role } from "discord-types/general"; +import type { ReactNode } from "react"; + +import { PermissionsSortOrder, settings } from "."; + +export const cl = classNameFactory("vc-permviewer-"); + +function formatPermissionWithoutMatchingString(permission: string) { + return wordsToTitle(permission.toLowerCase().split("_")); +} + +// because discord is unable to be consistent with their names +const PermissionKeyMap = { + MANAGE_GUILD: "MANAGE_SERVER", + MANAGE_GUILD_EXPRESSIONS: "MANAGE_EXPRESSIONS", + CREATE_GUILD_EXPRESSIONS: "CREATE_EXPRESSIONS", + MODERATE_MEMBERS: "MODERATE_MEMBER", // HELLOOOO ?????? + STREAM: "VIDEO", + SEND_VOICE_MESSAGES: "ROLE_PERMISSIONS_SEND_VOICE_MESSAGE", +} as const; + +export function getPermissionString(permission: string) { + permission = PermissionKeyMap[permission] || permission; + + return i18n.Messages[permission] || + // shouldn't get here but just in case + formatPermissionWithoutMatchingString(permission); +} + +export function getPermissionDescription(permission: string): ReactNode { + // DISCORD PLEEEEEEEEAAAAASE IM BEGGING YOU :( + if (permission === "USE_APPLICATION_COMMANDS") + permission = "USE_APPLICATION_COMMANDS_GUILD"; + else if (permission === "SEND_VOICE_MESSAGES") + permission = "SEND_VOICE_MESSAGE_GUILD"; + else if (permission !== "STREAM") + permission = PermissionKeyMap[permission] || permission; + + const msg = i18n.Messages[`ROLE_PERMISSIONS_${permission}_DESCRIPTION`] as any; + if (msg?.hasMarkdown) + return Parser.parse(msg.message); + + if (typeof msg === "string") return msg; + + return ""; +} + +export function getSortedRoles({ roles, id }: Guild, member: GuildMember) { + return [...member.roles, id] + .map(id => roles[id]) + .sort((a, b) => b.position - a.position); +} + +export function sortUserRoles(roles: Role[]) { + switch (settings.store.permissionsSortOrder) { + case PermissionsSortOrder.HighestRole: + return roles.sort((a, b) => b.position - a.position); + case PermissionsSortOrder.LowestRole: + return roles.sort((a, b) => a.position - b.position); + default: + return roles; + } +} diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 1534f329..51b3ceee 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -85,6 +85,51 @@ export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: getAPIBaseURL(withVersion?: boolean): string; }; +export type Permissions = "CREATE_INSTANT_INVITE" + | "KICK_MEMBERS" + | "BAN_MEMBERS" + | "ADMINISTRATOR" + | "MANAGE_CHANNELS" + | "MANAGE_GUILD" + | "CHANGE_NICKNAME" + | "MANAGE_NICKNAMES" + | "MANAGE_ROLES" + | "MANAGE_WEBHOOKS" + | "MANAGE_GUILD_EXPRESSIONS" + | "VIEW_AUDIT_LOG" + | "VIEW_CHANNEL" + | "VIEW_GUILD_ANALYTICS" + | "VIEW_CREATOR_MONETIZATION_ANALYTICS" + | "MODERATE_MEMBERS" + | "SEND_MESSAGES" + | "SEND_TTS_MESSAGES" + | "MANAGE_MESSAGES" + | "EMBED_LINKS" + | "ATTACH_FILES" + | "READ_MESSAGE_HISTORY" + | "MENTION_EVERYONE" + | "USE_EXTERNAL_EMOJIS" + | "ADD_REACTIONS" + | "USE_APPLICATION_COMMANDS" + | "MANAGE_THREADS" + | "CREATE_PUBLIC_THREADS" + | "CREATE_PRIVATE_THREADS" + | "USE_EXTERNAL_STICKERS" + | "SEND_MESSAGES_IN_THREADS" + | "CONNECT" + | "SPEAK" + | "MUTE_MEMBERS" + | "DEAFEN_MEMBERS" + | "MOVE_MEMBERS" + | "USE_VAD" + | "PRIORITY_SPEAKER" + | "STREAM" + | "USE_EMBEDDED_ACTIVITIES" + | "REQUEST_TO_SPEAK" + | "MANAGE_EVENTS"; + +export type PermissionsBits = Record; + export interface Locale { name: string; value: string; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 3893a3a1..629d052c 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -114,3 +114,5 @@ waitFor("parseTopic", m => Parser = m); export let SettingsRouter: any; waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); + +export const PermissionsBits: t.PermissionsBits = findLazy(m => typeof m.ADMINISTRATOR === "bigint");