diff --git a/src/plugins/moreUserTags.ts b/src/plugins/moreUserTags.ts
new file mode 100644
index 000000000..ba2a9b762
--- /dev/null
+++ b/src/plugins/moreUserTags.ts
@@ -0,0 +1,275 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 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 { definePluginSettings, migratePluginSettings } from "@api/settings";
+import { Devs } from "@utils/constants";
+import { proxyLazy } from "@utils/proxyLazy.js";
+import definePlugin, { OptionType } from "@utils/types";
+import { find, findByPropsLazy } from "@webpack";
+import { ChannelStore, GuildStore } from "@webpack/common";
+import { Channel, Message, User } from "discord-types/general";
+
+type PermissionName = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" | "BAN_MEMBERS" | "ADMINISTRATOR" | "MANAGE_CHANNELS" | "MANAGE_GUILD" | "CHANGE_NICKNAME" | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" | "MANAGE_GUILD_EXPRESSIONS" | "CREATE_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" | "USE_SOUNDBOARD" | "USE_EXTERNAL_SOUNDS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "CREATE_EVENTS";
+
+interface Tag {
+ // name used for identifying, must be alphanumeric + underscores
+ name: string;
+ // name shown on the tag itself, can be anything probably; automatically uppercase'd
+ displayName: string;
+ description: string;
+ permissions?: PermissionName[];
+ condition?(message: Message | null, user: User, channel: Channel): boolean;
+}
+
+const CLYDE_ID = "1081004946872352958";
+
+// PermissionStore.computePermissions is not the same function and doesn't work here
+const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as {
+ computePermissions({ ...args }): bigint;
+};
+
+const Permissions = findByPropsLazy("SEND_MESSAGES", "VIEW_CREATOR_MONETIZATION_ANALYTICS") as Record;
+const Tags = proxyLazy(() => find(m => m.Types?.[0] === "BOT").Types) as Record;
+
+const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
+
+const tags: Tag[] = [
+ {
+ name: "WEBHOOK",
+ displayName: "Webhook",
+ description: "Messages sent by webhooks",
+ condition: isWebhook
+ }, {
+ name: "OWNER",
+ displayName: "Owner",
+ description: "Owns the server",
+ condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
+ }, {
+ name: "ADMINISTRATOR",
+ displayName: "Admin",
+ description: "Has the administrator permission",
+ permissions: ["ADMINISTRATOR"]
+ }, {
+ name: "MODERATOR_STAFF",
+ displayName: "Staff",
+ description: "Can manage the server, channels or roles",
+ permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
+ }, {
+ name: "MODERATOR",
+ displayName: "Mod",
+ description: "Can manage messages or kick/ban people",
+ permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
+ }, {
+ name: "VOICE_MODERATOR",
+ displayName: "VC Mod",
+ description: "Can manage voice chats",
+ permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
+ }
+];
+
+const settings = definePluginSettings({
+ dontShowBotTag: {
+ description: "Don't show [BOT] text for bots with other tags (verified bots will still have checkmark)",
+ type: OptionType.BOOLEAN
+ },
+ ...Object.fromEntries(tags.map(({ name, displayName, description }) => [
+ `visibility_${name}`, {
+ description: `Show ${displayName} tags (${description})`,
+ type: OptionType.SELECT,
+ options: [
+ {
+ label: "Always",
+ value: "always",
+ default: true
+ }, {
+ label: "Only in chat",
+ value: "chat"
+ }, {
+ label: "Only in member list and profiles",
+ value: "not-chat"
+ }, {
+ label: "Never",
+ value: "never"
+ }
+ ]
+ }
+ ]))
+});
+
+migratePluginSettings("MoreUserTags", "Webhook Tags");
+export default definePlugin({
+ name: "MoreUserTags",
+ description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
+ authors: [Devs.Cyn, Devs.TheSun],
+ settings,
+ patches: [
+ // add tags to the tag list
+ {
+ find: '.BOT=0]="BOT"',
+ replacement: [
+ // add tags to the exported tags list (the Tags variable here)
+ {
+ match: /(\i)\[.\.BOT=0\]="BOT";/,
+ replace: "$&$1=$self.addTagVariants($1);"
+ },
+ // make the tag show the right text
+ {
+ match: /(switch\((\i)\){.+?)case (\i)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/,
+ replace: (_, origSwitch, variant, tags, displayedText, strings) =>
+ `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
+ },
+ // show OP tags correctly
+ {
+ match: /(\i)=(\i)===\i\.ORIGINAL_POSTER/,
+ replace: "$1=$self.isOPTag($2)"
+ }
+ ],
+ },
+ // in messages
+ {
+ find: ".Types.ORIGINAL_POSTER",
+ replacement: {
+ match: /return null==(\i)\?null:\(0,/,
+ replace: "$1=$self.getTag({...arguments[0],origType:$1,location:'chat'});$&"
+ }
+ },
+ // in the member list
+ {
+ find: ".renderBot=function(){",
+ replacement: {
+ match: /this.props.user;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag/,
+ replace: "this.props.user;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});\
+return type!==null?$2.botTag,type"
+ }
+ },
+ // pass channel id down props to be used in profiles
+ {
+ find: ".hasAvatarForGuild(null==",
+ replacement: {
+ match: /\.usernameSection,user/,
+ replace: ".usernameSection,moreTags_channelId:arguments[0].channelId,user"
+ }
+ },
+ {
+ find: 'copyMetaData:"User Tag"',
+ replacement: {
+ match: /discriminatorClass:(.{1,100}),botClass:/,
+ replace: "discriminatorClass:$1,moreTags_channelId:arguments[0].moreTags_channelId,botClass:"
+ }
+ },
+ // in profiles
+ {
+ find: ",botType:",
+ replacement: {
+ match: /,botType:(\i\((\i)\)),/g,
+ replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'}),"
+ }
+ },
+ ],
+
+ getPermissions(user: User, channel: Channel): string[] {
+ const guild = GuildStore.getGuild(channel?.guild_id);
+ if (!guild) return [];
+
+ const permissions = PermissionUtil.computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
+ return Object.entries(Permissions)
+ .map(([perm, permInt]) =>
+ permissions & permInt ? perm : ""
+ )
+ .filter(Boolean);
+ },
+
+ addTagVariants(val: any /* i cant think of a good name */) {
+ let i = 100;
+ tags.forEach(({ name }) => {
+ val[name] = ++i;
+ val[i] = name;
+ val[`${name}-BOT`] = ++i;
+ val[i] = `${name}-BOT`;
+ val[`${name}-OP`] = ++i;
+ val[i] = `${name}-OP`;
+ });
+ return val;
+ },
+
+ isOPTag: (tag: number) => tag === Tags.ORIGINAL_POSTER || tags.some(t => tag === Tags[`${t.name}-OP`]),
+
+ getTagText(passedTagName: string, strings: Record) {
+ if (!passedTagName) return "BOT";
+ const [tagName, variant] = passedTagName.split("-");
+ const tag = tags.find(({ name }) => tagName === name);
+ if (!tag) return "BOT";
+ switch (variant) {
+ case "OP":
+ return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tag.displayName}`;
+ case "BOT":
+ return `${strings.BOT_TAG_BOT} • ${tag.displayName}`;
+ default:
+ return tag.displayName;
+ }
+ },
+
+ getTag({
+ message, user, channelId, origType, location, channel
+ }: {
+ message?: Message,
+ user: User,
+ channel?: Channel & { isForumPost(): boolean; },
+ channelId?: string;
+ origType?: number;
+ location: string;
+ }): number | null {
+ if (location === "chat" && user.id === "1")
+ return Tags.OFFICIAL;
+ if (user.id === CLYDE_ID)
+ return Tags.AI;
+
+ let type = typeof origType === "number" ? origType : null;
+
+ channel ??= ChannelStore.getChannel(channelId!) as any;
+ if (!channel) return type;
+
+ const settings = this.settings.store;
+ const perms = this.getPermissions(user, channel);
+
+ for (const tag of tags) {
+ switch (settings[`visibility_${tag.name}`]) {
+ case "always":
+ case location:
+ break;
+ default:
+ continue;
+ }
+
+ if (
+ tag.permissions?.some(perm => perms.includes(perm)) ||
+ (tag.condition?.(message!, user, channel))
+ ) {
+ if (channel.isForumPost() && channel.ownerId === user.id)
+ type = Tags[`${tag.name}-OP`];
+ else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag)
+ type = Tags[`${tag.name}-BOT`];
+ else
+ type = Tags[tag.name];
+ break;
+ }
+ }
+
+ return type;
+ }
+});
diff --git a/src/plugins/webhookTags.ts b/src/plugins/webhookTags.ts
deleted file mode 100644
index 96cbf3854..000000000
--- a/src/plugins/webhookTags.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 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 { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
-
-export default definePlugin({
- name: "Webhook Tags",
- description: "Changes the bot tag to say webhook for webhooks",
- authors: [Devs.Cyn],
- patches: [
- {
- find: '.BOT=0]="BOT"',
- replacement: [
- {
- match: /(.)\[.\.BOT=0\]="BOT";/,
- replace: (orig, types) =>
- `${types}[${types}.WEBHOOK=99]="WEBHOOK";${orig}`,
- },
- {
- match: /case (.)\.BOT:default:(.)=/,
- replace: (orig, types, text) =>
- `case ${types}.WEBHOOK:${text}="WEBHOOK";break;${orig}`,
- },
- ],
- },
- {
- find: ".Types.ORIGINAL_POSTER",
- replacement: {
- match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3}\.\i)/,
- replace: (orig, type, BotTag) =>
- `if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Types.WEBHOOK}${orig}`,
- },
- },
- ],
-});