diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Icons.tsx | 40 | ||||
-rw-r--r-- | src/components/iconStyles.css | 4 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx | 225 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/components/UserPermissions.tsx | 175 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/components/icons.tsx | 58 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/index.tsx | 180 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/styles.css | 128 | ||||
-rw-r--r-- | src/plugins/permissionsViewer/utils.ts | 84 | ||||
-rw-r--r-- | src/webpack/common/types/utils.d.ts | 45 | ||||
-rw-r--r-- | src/webpack/common/utils.ts | 2 |
10 files changed, 937 insertions, 4 deletions
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index d1cc7a6..4227fce 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<SVGSVGElement> { className?: string; height?: number; width?: number; } -function Icon({ height = 24, width = 24, className, children, viewBox }: PropsWithChildren<BaseIconProps>) { +function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) { return ( <svg className={classes(className, "vc-icon")} - aria-hidden="true" role="img" width={width} height={height} viewBox={viewBox} + {...svgProps} > {children} </svg> @@ -114,3 +115,34 @@ export function ImageIcon(props: IconProps) { </Icon> ); } + +export function InfoIcon(props: IconProps) { + return ( + <Icon + {...props} + className={classes(props.className, "vc-info-icon")} + viewBox="0 0 12 12" + > + <path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z" /> + </Icon> + ); +} + +export function OwnerCrownIcon(props: IconProps) { + return ( + <Icon + aria-label={i18n.Messages.GUILD_OWNER} + {...props} + className={classes(props.className, "vc-owner-crown-icon")} + role="img" + viewBox="0 0 16 16" + > + <path + fill="currentColor" + fill-rule="evenodd" + clip-rule="evenodd" + d="M13.6572 5.42868C13.8879 5.29002 14.1806 5.30402 14.3973 5.46468C14.6133 5.62602 14.7119 5.90068 14.6473 6.16202L13.3139 11.4954C13.2393 11.7927 12.9726 12.0007 12.6666 12.0007H3.33325C3.02725 12.0007 2.76058 11.792 2.68592 11.4954L1.35258 6.16202C1.28792 5.90068 1.38658 5.62602 1.60258 5.46468C1.81992 5.30468 2.11192 5.29068 2.34325 5.42868L5.13192 7.10202L7.44592 3.63068C7.46173 3.60697 7.48377 3.5913 7.50588 3.57559C7.5192 3.56612 7.53255 3.55663 7.54458 3.54535L6.90258 2.90268C6.77325 2.77335 6.77325 2.56068 6.90258 2.43135L7.76458 1.56935C7.89392 1.44002 8.10658 1.44002 8.23592 1.56935L9.09792 2.43135C9.22725 2.56068 9.22725 2.77335 9.09792 2.90268L8.45592 3.54535C8.46794 3.55686 8.48154 3.56651 8.49516 3.57618C8.51703 3.5917 8.53897 3.60727 8.55458 3.63068L10.8686 7.10202L13.6572 5.42868ZM2.66667 12.6673H13.3333V14.0007H2.66667V12.6673Z" + /> + </Icon> + ); +} diff --git a/src/components/iconStyles.css b/src/components/iconStyles.css index 9f2ef8e..ca4075d 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 0000000..7a65a07 --- /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 <https://www.gnu.org/licenses/>. +*/ + +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<RoleOrUserPermission>, guild: Guild, header: string) { + return openModal(modalProps => ( + <RolesAndUsersPermissions + modalProps={modalProps} + permissions={permissions} + guild={guild} + header={header} + /> + )); +} + +function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; 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 ( + <ModalRoot + {...modalProps} + size={ModalSize.LARGE} + > + <ModalHeader> + <Text className={cl("perms-title")} variant="heading-lg/semibold">{header} permissions:</Text> + <ModalCloseButton onClick={modalProps.onClose} /> + </ModalHeader> + + <ModalContent> + {!selectedItem && ( + <div className={cl("perms-no-perms")}> + <Text variant="heading-lg/normal">No permissions to display!</Text> + </div> + )} + + {selectedItem && ( + <div className={cl("perms-container")}> + <div className={cl("perms-list")}> + {permissions.map((permission, index) => { + const user = UserStore.getUser(permission.id ?? ""); + const role = guild.roles[permission.id ?? ""]; + + return ( + <button + className={cl("perms-list-item-btn")} + onClick={() => selectItem(index)} + > + <div + className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} + onContextMenu={e => { + if (permission.type === PermissionType.Role) + ContextMenu.open(e, () => ( + <RoleContextMenu + guild={guild} + roleId={permission.id!} + onClose={modalProps.onClose} + /> + )); + }} + > + {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( + <span + className={cl("perms-role-circle")} + style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }} + /> + )} + {permission.type === PermissionType.User && user !== undefined && ( + <img + className={cl("perms-user-img")} + src={user.getAvatarURL(void 0, void 0, false)} + /> + )} + <Text variant="text-md/normal"> + { + permission.type === PermissionType.Role + ? role?.name || "Unknown Role" + : permission.type === PermissionType.User + ? user?.tag || "Unknown User" + : ( + <Flex style={{ gap: "0.2em", justifyItems: "center" }}> + @owner + <OwnerCrownIcon + height={18} + width={18} + aria-hidden="true" + /> + </Flex> + ) + } + </Text> + </div> + </button> + ); + })} + </div> + <div className={cl("perms-perms")}> + {Object.entries(PermissionsBits).map(([permissionName, bit]) => ( + <div className={cl("perms-perms-item")}> + <div className={cl("perms-perms-item-icon")}> + {(() => { + 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(); + })()} + </div> + <Text variant="text-md/normal">{getPermissionString(permissionName)}</Text> + + <Tooltip text={getPermissionDescription(permissionName) || "No Description"}> + {props => <InfoIcon {...props} />} + </Tooltip> + </div> + ))} + </div> + </div> + )} + </ModalContent> + </ModalRoot > + ); +} + +function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: string; onClose: () => void; }) { + return ( + <Menu.Menu + navId={cl("role-context-menu")} + onClose={ContextMenu.close} + aria-label="Role Options" + > + <Menu.MenuItem + id="vc-pw-view-as-role" + label="View As Role" + action={() => { + const role = guild.roles[roleId]; + if (!role) return; + + onClose(); + + FluxDispatcher.dispatch({ + type: "IMPERSONATE_UPDATE", + guildId: guild.id, + data: { + type: "ROLES", + roles: { + [roleId]: role + } + } + }); + }} + /> + </Menu.Menu> + ); +} + +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 0000000..acffa78 --- /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 <https://www.gnu.org/licenses/>. +*/ + +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<UserPermission>; + +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<RoleOrUserPermission> = 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 ( + <div> + <div className={cl("userperms-title-container")}> + <Text className={cl("userperms-title")} variant="eyebrow">Permissions</Text> + + <div> + <Tooltip text="Role Details"> + {tooltipProps => ( + <button + {...tooltipProps} + className={cl("userperms-permdetails-btn")} + onClick={() => + openRolesAndUsersPermissionsModal( + rolePermissions, + guild, + guildMember.nick || UserStore.getUser(guildMember.userId).username + ) + } + > + <svg + width="24" + height="24" + viewBox="0 0 24 24" + > + <path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" /> + </svg> + </button> + )} + </Tooltip> + + <Tooltip text={viewPermissions ? "Hide Permissions" : "View Permissions"}> + {tooltipProps => ( + <button + {...tooltipProps} + className={cl("userperms-toggleperms-btn")} + onClick={() => setViewPermissions(v => !v)} + > + <svg + width="24" + height="24" + viewBox="0 0 24 24" + transform={viewPermissions ? "scale(1 -1)" : "scale(1 1)"} + > + <path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" /> + </svg> + </button> + )} + </Tooltip> + </div> + </div> + + {viewPermissions && userPermissions.length > 0 && ( + <div className={classes(root, roles)}> + {userPermissions.map(({ permission, roleColor }) => ( + <div className={classes(role, rolePill, rolePillBorder)}> + <div className={roleRemoveButton}> + <span + className={roleCircle} + style={{ backgroundColor: roleColor }} + /> + </div> + <div className={roleName}> + <Text + className={roleNameOverflow} + variant="text-xs/medium" + > + {permission} + </Text> + </div> + </div> + ))} + </div> + )} + </div> + ); +} + +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 0000000..8b58a44 --- /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 <https://www.gnu.org/licenses/>. +*/ + +export function PermissionDeniedIcon() { + return ( + <svg + height="24" + width="24" + viewBox="0 0 24 24" + > + <title>Denied</title> + <path fill="var(--status-danger)" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" /> + </svg> + ); +} + +export function PermissionAllowedIcon() { + return ( + <svg + height="24" + width="24" + viewBox="0 0 24 24" + > + <title>Allowed</title> + <path fill="var(--text-positive)" d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ" /> + </svg> + ); +} + +export function PermissionDefaultIcon() { + return ( + <svg + height="24" + width="24" + viewBox="0 0 16 16" + > + <g> + <title>Not overwritten</title> + <polygon fill="var(--text-normal)" points="12 2.32 10.513 2 4 13.68 5.487 14" /> + </g> + </svg> + ); +} diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx new file mode 100644 index 0000000..793105a --- /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 <https://www.gnu.org/licenses/>. +*/ + +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 ( + <Menu.MenuItem + id="perm-viewer-permissions" + label="Permissions" + action={() => { + 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) => <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 0000000..6d6c137 --- /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 0000000..b747147 --- /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 <https://www.gnu.org/licenses/>. +*/ + +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 1534f32..51b3cee 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<Permissions, bigint>; + export interface Locale { name: string; value: string; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 3893a3a..629d052 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"); |