Plugin

PermissionsViewer

View the permissions a user or channel has, and the roles of a server

Servers Roles Utility
index.tsx
Download

Source

src/plugins/permissionsViewer/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./styles.css";
8
9import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
10import { definePluginSettings } from "@api/Settings";
11import ErrorBoundary from "@components/ErrorBoundary";
12import { SafetyIcon } from "@components/Icons";
13import { TooltipContainer } from "@components/TooltipContainer";
14import { Devs } from "@utils/constants";
15import { classes } from "@utils/misc";
16import definePlugin, { OptionType } from "@utils/types";
17import type { Guild, RoleOrUserPermission } from "@vencord/discord-types";
18import { PermissionOverwriteType } from "@vencord/discord-types/enums";
19import { findCssClassesLazy } from "@webpack";
20import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useEffect, useRef, UserStore } from "@webpack/common";
21
22import openRolesAndUsersPermissionsModal from "./components/RolesAndUsersPermissions";
23import UserPermissions from "./components/UserPermissions";
24import { getSortedRolesForMember, loadGetGuildPermissionSpecMap, sortPermissionOverwrites } from "./utils";
25
26const PopoutClasses = findCssClassesLazy("container", "popoutRoleDot");
27
28export const enum PermissionsSortOrder {
29 HighestRole,
30 LowestRole
31}
32
33const enum MenuItemParentType {
34 User,
35 Channel,
36 Guild
37}
38
39export const settings = definePluginSettings({
40 permissionsSortOrder: {
41 description: "The sort method used for defining which role grants an user a certain permission",
42 type: OptionType.SELECT,
43 options: [
44 { label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true },
45 { label: "Lowest Role", value: PermissionsSortOrder.LowestRole }
46 ]
47 },
48});
49
50function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
51 if (type === MenuItemParentType.User && !GuildMemberStore.isMember(guildId, id!)) return null;
52
53 return (
54 <Menu.MenuItem
55 id="perm-viewer-permissions"
56 label="Permissions"
57 action={() => {
58 const guild = GuildStore.getGuild(guildId);
59
60 const { permissions, header } = match(type)
61 .returnType<{ permissions: RoleOrUserPermission[], header: string; }>()
62 .with(MenuItemParentType.User, () => {
63 const member = GuildMemberStore.getMember(guildId, id!)!;
64
65 const permissions: RoleOrUserPermission[] = getSortedRolesForMember(guild, member)
66 .map(role => ({
67 type: PermissionOverwriteType.ROLE,
68 ...role
69 }));
70
71 if (guild.ownerId === id) {
72 permissions.push({
73 type: PermissionOverwriteType.OWNER,
74 permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n)
75 });
76 }
77
78 return {
79 permissions,
80 header: member.nick ?? UserStore.getUser(member.userId).username
81 };
82 })
83 .with(MenuItemParentType.Channel, () => {
84 const channel = ChannelStore.getChannel(id!);
85
86 const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
87 type,
88 id,
89 overwriteAllow: allow,
90 overwriteDeny: deny
91 })), guildId);
92
93 return {
94 permissions,
95 header: channel.name
96 };
97 })
98 .otherwise(() => {
99 const permissions = GuildRoleStore.getSortedRoles(guild.id).map(role => ({
100 type: PermissionOverwriteType.ROLE,
101 ...role
102 }));
103
104 return {
105 permissions,
106 header: guild.name
107 };
108 });
109
110 openRolesAndUsersPermissionsModal(permissions, guild, header);
111 }}
112 />
113 );
114}
115
116function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
117 return (children, props) => {
118 if (
119 !props ||
120 (type === MenuItemParentType.User && !props.user) ||
121 (type === MenuItemParentType.Guild && !props.guild) ||
122 (type === MenuItemParentType.Channel && (!props.channel || !props.guild))
123 ) {
124 return;
125 }
126
127 const group = findGroupChildrenByChildId(childId, children);
128
129 const item = match(type)
130 .with(MenuItemParentType.User, () => MenuItem(props.guildId, props.user.id, type))
131 .with(MenuItemParentType.Channel, () => MenuItem(props.guild.id, props.channel.id, type))
132 .with(MenuItemParentType.Guild, () => MenuItem(props.guild.id))
133 .otherwise(() => null);
134
135 if (item == null) return;
136
137 if (group) {
138 return group.push(item);
139 }
140
141 // "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID"
142 if (childId === "roles" && props.guildId) {
143 children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>);
144 }
145 };
146}
147
148export default definePlugin({
149 name: "PermissionsViewer",
150 description: "View the permissions a user or channel has, and the roles of a server",
151 tags: ["Servers", "Roles", "Utility"],
152 authors: [Devs.Nuckyz, Devs.Ven],
153 settings,
154
155 patches: [
156 {
157 find: "#{intl::COLLAPSE_ROLES}",
158 replacement: {
159 match: /(?<=\i\.id\)\),\i\(\))(?=,\i\?)/,
160 replace: ",$self.ViewPermissionsButton(arguments[0])"
161 }
162 }
163 ],
164
165 ViewPermissionsButton: ErrorBoundary.wrap(({ className, guild, userId }: { className: string; guild: Guild; userId: string; }) => {
166 const buttonRef = useRef(null);
167 useEffect(() => void loadGetGuildPermissionSpecMap(), []);
168
169 const guildMember = GuildMemberStore.getMember(guild.id, userId);
170 if (!guildMember) return null;
171
172 return (
173 <Popout
174 position="bottom"
175 align="center"
176 targetElementRef={buttonRef}
177 renderPopout={({ closePopout }) => (
178 <Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
179 <UserPermissions guild={guild} guildMember={guildMember} closePopout={closePopout} />
180 </Dialog>
181 )}
182 >
183 {popoutProps => (
184 <TooltipContainer text="View Permissions">
185 <Button
186 {...popoutProps}
187 ref={buttonRef}
188 color={Button.Colors.CUSTOM}
189 look={Button.Looks.FILLED}
190 size={Button.Sizes.NONE}
191 className={classes(className, "vc-permviewer-role-button")}
192 >
193 <SafetyIcon height="16" width="16" />
194 </Button>
195 </TooltipContainer>
196 )}
197 </Popout>
198 );
199 }, { noop: true }),
200
201 contextMenus: {
202 "user-context": makeContextMenuPatch("roles", MenuItemParentType.User),
203 "channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
204 "guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild),
205 "guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild)
206 }
207});
208