feat(plugins): Permissions Viewer (#477)
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
parent
9c1b3a9afd
commit
64b38348d4
|
@ -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!)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.vc-open-external-icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.vc-owner-crown-icon {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
|
|
@ -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;
|
175
src/plugins/permissionsViewer/components/UserPermissions.tsx
Normal file
175
src/plugins/permissionsViewer/components/UserPermissions.tsx
Normal file
|
@ -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 });
|
58
src/plugins/permissionsViewer/components/icons.tsx
Normal file
58
src/plugins/permissionsViewer/components/icons.tsx
Normal file
|
@ -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>
|
||||
);
|
||||
}
|
180
src/plugins/permissionsViewer/index.tsx
Normal file
180
src/plugins/permissionsViewer/index.tsx
Normal file
|
@ -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);
|
||||
},
|
||||
});
|
128
src/plugins/permissionsViewer/styles.css
Normal file
128
src/plugins/permissionsViewer/styles.css
Normal file
|
@ -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);
|
||||
}
|
84
src/plugins/permissionsViewer/utils.ts
Normal file
84
src/plugins/permissionsViewer/utils.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
45
src/webpack/common/types/utils.d.ts
vendored
45
src/webpack/common/types/utils.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue