Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moreUserTags: attempt to rewrite #3170

Draft
wants to merge 27 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7740d9a
moreUserTags: rewrite
henmalib Jan 27, 2025
b74e5e2
moreUserTags: readable tags/devs
henmalib Jan 27, 2025
97f540c
no more lint fails for you, sadan
henmalib Jan 27, 2025
c219cdc
moreUserTags: removed 'add tags' patch
henmalib Jan 27, 2025
f9ab7d3
moreUserTags: rename obj/format
henmalib Jan 28, 2025
ae1ed25
moreUserTags: memberList
henmalib Jan 28, 2025
9ea2f6f
moreUserTags: profile support
henmalib Jan 28, 2025
6ef9712
moreUserTags: better typing
henmalib Jan 29, 2025
4370dd4
Merge branch 'main' into tags
henmalib Jan 29, 2025
7ea9677
moreUserTags: use decorations
henmalib Jan 29, 2025
3e855ff
Merge branch 'tags' of github.com:henmalib/Vencord into tags
henmalib Jan 29, 2025
f22d9fc
moreUserTags: revert to patching
henmalib Jan 29, 2025
142fd3e
Merge branch 'main' into tags
henmalib Feb 3, 2025
5a6ea90
moreUserTags: use decorations
henmalib Feb 3, 2025
e2b0e25
moreUserTags: cleanup
henmalib Feb 3, 2025
bf181ec
Merge branch 'dev' into tags
henmalib Feb 4, 2025
ac1c720
moreUserTags: change classes
henmalib Feb 4, 2025
4810df4
moreUserTags: fix center offset
henmalib Feb 4, 2025
8d1cab9
Fix alignment for tags in messages
Nuckyz Feb 4, 2025
a04a97c
moreUserTags: generate settings if not present
henmalib Feb 4, 2025
06c2ba5
moreUserTags: removed unused const
henmalib Feb 4, 2025
90f2e46
moreUserTags: actually use settings
henmalib Feb 5, 2025
d474337
moreUserTags: oops
henmalib Feb 5, 2025
e30cc47
Merge branch 'dev' into tags
henmalib Feb 5, 2025
29512a5
moreUserTags: use proper height
henmalib Feb 5, 2025
97676c6
balls
henmalib Feb 5, 2025
ddd6668
moreUserTags: use classFactory
henmalib Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/plugins/moreUserTags/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { findByCodeLazy, findLazy } from "@webpack";
import { GuildStore } from "@webpack/common";
import { RC } from "@webpack/types";
import { Channel, Guild, Message, User } from "discord-types/general";

import type { ITag } from "./types";

export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
export const tags: ITag[] = [
{
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"]
}, {
name: "CHAT_MODERATOR",
displayName: "Chat Mod",
description: "Can timeout people",
permissions: ["MODERATE_MEMBERS"]
}
];

export const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };

// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
export const computePermissions: (options: {
user?: { id: string; } | string | null;
context?: Guild | Channel | null;
overwrites?: Channel["permissionOverwrites"] | null;
checkElevated?: boolean /* = true */;
excludeGuildPermissions?: boolean /* = false */;
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
184 changes: 184 additions & 0 deletions src/plugins/moreUserTags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin from "@utils/types";
import { ChannelStore, GuildStore, PermissionsBits } from "@webpack/common";
import { Channel, Message, User } from "discord-types/general";

import { computePermissions, isWebhook, Tag, tags } from "./consts";
import { settings } from "./settings";

const genTagTypes = () => {
let i = 100;
const obj = {};

for (const { name } of tags) {
obj[name] = ++i;
obj[i] = name;
obj[`${name}-BOT`] = ++i;
obj[i] = `${name}-BOT`;
obj[`${name}-OP`] = ++i;
obj[i] = `${name}-OP`;
}

return obj;
};

export default definePlugin({
name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, Devs.hen],
settings,
patches: [
// Render Tags in messages
// Maybe there is a better way to catch this horror
{
find: ".isVerifiedBot(),hideIcon:",
replacement: {
match: /(?<=let (\i).{1,500}.isSystemDM.{0,350}),null==(\i\))(?=.{1,30}?null:)/,
replace:
",($1=$self.getTag({...arguments[0],isChat:true,origType:$1}))$&",
},
},
// Make discord actually use our tags
{
find: ".STAFF_ONLY_DM:",
replacement: {
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})default:(\i)=/,
replace: "default:$2=$self.getTagText($self.localTags[$1]);",
},
},
// Member list
// In the current state it makes smth like
// null != U && U && ($1=blahblahblah)
{
find: ".lostPermission)",
replacement: {
match: /(?<=return .{0,20})\.bot?(?=.{0,100}type:(\i))/,
replace: "&& ($1=$self.getTag({...arguments[0],isChat:false,origType:$1}))"
}
},

// Next both 2 patches are goint together
// First one passes down the react dom channelId which is required to get tag
// Second one actually gets/displays it
{
find: ".hasAvatarForGuild(null==",
replacement: {
match: /user:\i,(?=.{0,50}.BITE_SIZE)/,
replace: "$&channelId:arguments[0].channelId,"
},
},
{
find: ".clickableUsername",
replacement: {
match: /null!=(\i)(?=.{0,100}type:\i)/,
replace: "($1=$self.getTag({...arguments[0],isChat:false,origType:$1}),$1!==null)"
}
}
],
localTags: genTagTypes(),

getTagText(passedTagName: string) {
if (!passedTagName) return getIntlMessage("APP_TAG");
const [tagName, variant] = passedTagName.split("-");

const tag = tags.find(({ name }) => tagName === name);
if (!tag) return getIntlMessage("APP_TAG");

if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return getIntlMessage("APP_TAG");

const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
switch (variant) {
case "OP":
return `${getIntlMessage("BOT_TAG_FORUM_ORIGINAL_POSTER")} • ${tagText}`;
case "BOT":
return `${getIntlMessage("APP_TAG")} • ${tagText}`;
default:
return tagText;
}
},

getTag({
message, user, channelId, origType, isChat, channel, ...rest
}: {
message?: Message,
user: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
channelId?: string;
origType?: number;
isChat?: boolean;
}): number | null {
if (!user)
return null;
if (isChat && user.id === "1")
return Tag.Types.OFFICIAL;
if (user.isClyde())
return Tag.Types.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) {
if (isChat && !settings.tagSettings[tag.name].showInChat)
continue;
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
continue;

// If the owner tag is disabled, and the user is the owner of the guild,
// avoid adding other tags because the owner will always match the condition for them
if (
(tag.name !== "OWNER" &&
GuildStore.getGuild(channel?.guild_id)?.ownerId ===
user.id &&
isChat &&
!settings.tagSettings.OWNER.showInChat) ||
(!isChat &&
!settings.tagSettings.OWNER.showInNotChat)
)
continue;

if (
tag.permissions?.some(perm => perms.includes(perm)) ||
tag.condition?.(message!, user, channel)
) {
if ((channel.isForumPost() || channel.isMediaPost()) && channel.ownerId === user.id)
type = this.localTags[`${tag.name}-OP`];

else if (
user.bot &&
!isWebhook(message!, user) &&
!settings.dontShowBotTag
)
type = this.localTags[`${tag.name}-BOT`];

else type = this.localTags[tag.name];
break;
}
}

return type;
},
getPermissions(user: User, channel: Channel): string[] {
const guild = GuildStore.getGuild(channel?.guild_id);
if (!guild) return [];

const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
return Object.entries(PermissionsBits)
.map(([perm, permInt]) =>
permissions & permInt ? perm : ""
)
.filter(Boolean);
},
});

83 changes: 83 additions & 0 deletions src/plugins/moreUserTags/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { definePluginSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { OptionType } from "@utils/types";
import { Card, Flex, Forms, Switch, TextInput, Tooltip } from "@webpack/common";

import { Tag, tags } from "./consts";
import { TagSettings } from "./types";


const defaultSettings = Object.fromEntries(
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
) as TagSettings;

function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;

return (
<Flex flexDirection="column">
{tags.map(t => (
<Card key={t.name} style={{ padding: "1em 1em 0" }}>
<Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{t.displayName} Tag <Tag type={Tag.Types[t.name]} />
</div>
)}
</Tooltip>
</Forms.FormTitle>

<TextInput
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>

<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
</Switch>

<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
</Switch>
</Card>
))}
</Flex>
);
}

export const settings = definePluginSettings({
dontShowForBots: {
description: "Don't show extra tags for bots (excluding webhooks)",
type: OptionType.BOOLEAN
},
dontShowBotTag: {
description: "Only show extra tags for bots / Hide [BOT] text",
type: OptionType.BOOLEAN
},
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me"
}
});
34 changes: 34 additions & 0 deletions src/plugins/moreUserTags/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import type { Permissions } from "@webpack/types";
import type { Channel, Message, User } from "discord-types/general";

export interface ITag {
// 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?: Permissions[];
condition?(message: Message | null, user: User, channel: Channel): boolean;
}

export interface TagSetting {
text: string;
showInChat: boolean;
showInNotChat: boolean;
}
export interface TagSettings {
WEBHOOK: TagSetting,
OWNER: TagSetting,
ADMINISTRATOR: TagSetting,
MODERATOR_STAFF: TagSetting,
MODERATOR: TagSetting,
VOICE_MODERATOR: TagSetting,
TRIAL_MODERATOR: TagSetting,
[k: string]: TagSetting;
}
4 changes: 4 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n,
},
hen: {
id: 279266228151779329n,
name: "Hen"
}
} satisfies Record<string, Dev>);

// iife so #__PURE__ works correctly
Expand Down