Plugin
PermissionsViewer
View the permissions a user or channel has, and the roles of a server
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./styles.css";8
9
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";10
import { definePluginSettings } from "@api/Settings";11
import ErrorBoundary from "@components/ErrorBoundary";12
import { SafetyIcon } from "@components/Icons";13
import { TooltipContainer } from "@components/TooltipContainer";14
import { Devs } from "@utils/constants";15
import { classes } from "@utils/misc";16
import definePlugin, { OptionType } from "@utils/types";17
import type { Guild, RoleOrUserPermission } from "@vencord/discord-types";18
import { PermissionOverwriteType } from "@vencord/discord-types/enums";19
import { findCssClassesLazy } from "@webpack";20
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useEffect, useRef, UserStore } from "@webpack/common";21
22
import openRolesAndUsersPermissionsModal from "./components/RolesAndUsersPermissions";23
import UserPermissions from "./components/UserPermissions";24
import { getSortedRolesForMember, loadGetGuildPermissionSpecMap, sortPermissionOverwrites } from "./utils";25
26
const PopoutClasses = findCssClassesLazy("container", "popoutRoleDot");27
28
export const enum PermissionsSortOrder {29
HighestRole,30
LowestRole31
}32
33
const enum MenuItemParentType {34
User,35
Channel,36
Guild37
}38
39
export 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
50
function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {51
if (type === MenuItemParentType.User && !GuildMemberStore.isMember(guildId, id!)) return null;52
53
return (54
<Menu.MenuItem55
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
...role69
}));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).username81
};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: deny91
})), guildId);92
93
return {94
permissions,95
header: channel.name96
};97
})98
.otherwise(() => {99
const permissions = GuildRoleStore.getSortedRoles(guild.id).map(role => ({100
type: PermissionOverwriteType.ROLE,101
...role102
}));103
104
return {105
permissions,106
header: guild.name107
};108
});109
110
openRolesAndUsersPermissionsModal(permissions, guild, header);111
}}112
/>113
);114
}115
116
function 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
148
export 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
<Popout174
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
<Button186
{...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