FakeProfileThemes: fix error when own profile is not loaded (#3514)

Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
sadan4 2025-07-10 16:38:27 -04:00 committed by GitHub
parent 18f083b7e6
commit 6787e98003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 297 additions and 126 deletions

View file

@ -25,3 +25,8 @@ export const enum ApplicationCommandType {
USER = 2,
MESSAGE = 3,
}
export const enum ApplicationIntegrationType {
GUILD_INSTALL = 0,
USER_INSTALL = 1
}

View file

@ -0,0 +1,23 @@
import { User } from "./User";
export interface Application {
id: string;
name: string;
description?: string | null;
type: number | null;
icon: string | null | undefined;
is_discoverable: boolean;
is_monetized: boolean;
is_verified: boolean;
bot?: User;
deeplink_uri?: string;
flags?: number;
privacy_policy_url?: string;
terms_of_service_url?: string;
install_params?: ApplicationInstallParams;
}
export interface ApplicationInstallParams {
permissions: string | null;
scopes: string[];
}

View file

@ -6,7 +6,7 @@ export class User extends DiscordRecord {
constructor(user: object);
accentColor: number;
avatar: string;
banner: string;
banner: string | null | undefined;
bio: string;
bot: boolean;
desktop: boolean;
@ -27,7 +27,6 @@ export class User extends DiscordRecord {
system: boolean;
username: string;
verified: boolean;
themeColors?: [number, number];
get createdAt(): Date;
get hasPremiumPerks(): boolean;

View file

@ -1,3 +1,4 @@
export * from "./Application";
export * from "./Channel";
export * from "./Guild";
export * from "./GuildMember";

View file

@ -0,0 +1,151 @@
import { FluxStore, Guild, User, Application, ApplicationInstallParams } from "..";
import { ApplicationIntegrationType } from "../../enums";
export interface MutualFriend {
/**
* the userid of the mutual friend
*/
key: string;
/**
* the status of the mutual friend
*/
status: "online" | "offline" | "idle" | "dnd";
/**
* the user object of the mutual friend
*/
user: User;
}
export interface MutualGuild {
/**
* the guild object of the mutual guild
*/
guild: Guild;
/**
* the user's nickname in the guild, if any
*/
nick: string | null;
}
export interface ProfileBadge {
id: string;
description: string;
icon: string;
link?: string;
}
export interface ConnectedAccount {
type: "twitch" | "youtube" | "skype" | "steam" | "leagueoflegends" | "battlenet" | "bluesky" | "bungie" | "reddit" | "twitter" | "twitter_legacy" | "spotify" | "facebook" | "xbox" | "samsung" | "contacts" | "instagram" | "mastodon" | "soundcloud" | "github" | "playstation" | "playstation-stg" | "epicgames" | "riotgames" | "roblox" | "paypal" | "ebay" | "tiktok" | "crunchyroll" | "domain" | "amazon-music";
/**
* underlying id of connected account
* eg. account uuid
*/
id: string;
/**
* display name of connected account
*/
name: string;
verified: boolean;
metadata?: Record<string, string>;
}
export interface ProfileApplication {
id: string;
customInstallUrl: string | undefined;
installParams: ApplicationInstallParams | undefined;
flags: number;
popularApplicationCommandIds?: string[];
integrationTypesConfig: Record<ApplicationIntegrationType, Partial<{
oauth2_install_params: ApplicationInstallParams;
}>>;
primarySkuId: string | undefined;
storefront_available: boolean;
}
export interface UserProfileBase extends Pick<User, "banner"> {
accentColor: number | null;
/**
* often empty for guild profiles, get the user profile for badges
*/
badges: ProfileBadge[];
bio: string | undefined;
popoutAnimationParticleType: string | null;
profileEffectExpiresAt: number | Date | undefined;
profileEffectId: undefined | string;
/**
* often an empty string when not set
*/
pronouns: string | "" | undefined;
themeColors: [number, number] | undefined;
userId: string;
}
export interface ApplicationRoleConnection {
application: Application;
application_metadata: Record<string, any>;
metadata: Record<string, any>;
platform_name: string;
platform_username: string;
}
export interface UserProfile extends UserProfileBase, Pick<User, "premiumType"> {
/** If this is a bot user profile, this will be its application */
application: ProfileApplication | null;
applicationRoleConnections: ApplicationRoleConnection[] | undefined;
connectedAccounts: ConnectedAccount[] | undefined;
fetchStartedAt: number;
fetchEndedAt: number;
legacyUsername: string | undefined;
premiumGuildSince: Date | null;
premiumSince: Date | null;
}
export class UserProfileStore extends FluxStore {
/**
* @param userId the user ID of the profile being fetched.
* @param guildId the guild ID to of the profile being fetched.
* defaults to the internal symbol `NO GUILD ID` if nullish
*
* @returns true if the profile is being fetched, false otherwise.
*/
isFetchingProfile(userId: string, guildId?: string): boolean;
/**
* Check if mutual friends for {@link userId} are currently being fetched.
*
* @param userId the user ID of the mutual friends being fetched.
*
* @returns true if mutual friends are being fetched, false otherwise.
*/
isFetchingFriends(userId: string): boolean;
get isSubmitting(): boolean;
getUserProfile(userId: string): UserProfile | undefined;
getGuildMemberProfile(userId: string, guildId: string | undefined): UserProfileBase | null;
/**
* Get the mutual friends of a user.
*
* @param userId the user ID of the user to get the mutual friends of.
*
* @returns an array of mutual friends, or undefined if the user has no mutual friends
*/
getMutualFriends(userId: string): MutualFriend[] | undefined;
/**
* Get the count of mutual friends for a user.
*
* @param userId the user ID of the user to get the mutual friends count of.
*
* @returns the count of mutual friends, or undefined if the user has no mutual friends
*/
getMutualFriendsCount(userId: string): number | undefined;
/**
* Get the mutual guilds of a user.
*
* @param userId the user ID of the user to get the mutual guilds of.
*
* @returns an array of mutual guilds, or undefined if the user has no mutual guilds
*/
getMutualGuilds(userId: string): MutualGuild[] | undefined;
}

View file

@ -11,6 +11,7 @@ export * from "./RelationshipStore";
export * from "./SelectedChannelStore";
export * from "./SelectedGuildStore";
export * from "./ThemeStore";
export * from "./UserProfileStore";
export * from "./UserStore";
export * from "./WindowStore";

View file

@ -22,13 +22,14 @@ import "./index.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { fetchUserProfile } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { User, UserProfile } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { ReactElement } from "react";
import virtualMerge from "virtual-merge";
interface Colors {
@ -81,14 +82,6 @@ const settings = definePluginSettings({
}
});
interface ColorPickerProps {
color: number | null;
label: ReactElement<any>;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
// I can't be bothered to figure out the semantics of this component. The
// functions surely get some event argument sent to them and they likely aren't
// all required. If anyone who wants to use this component stumbles across this
@ -106,30 +99,15 @@ interface ProfileModalProps {
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
function SettingsAboutComponentWrapper() {
const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));
export default definePlugin({
name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
authors: [Devs.Alyxia, Devs.Remty],
patches: [
{
find: "UserProfileStore",
replacement: {
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
replace: "$self.colorDecodeHook($1)"
}
},
{
find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}",
replacement: {
match: /#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\).+?}\)(?=\])(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
}
}
],
settingsAboutComponent: () => {
return !userProfileLoading && <SettingsAboutComponent />;
}
function SettingsAboutComponent() {
const existingColors = decode(
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id)?.bio ?? ""
) ?? [0, 0];
const [color1, setColor1] = useState(existingColors[0]);
const [color2, setColor2] = useState(existingColors[1]);
@ -214,10 +192,34 @@ export default definePlugin({
</div>
</Forms.FormText>
</Forms.FormSection>);
}
export default definePlugin({
name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
authors: [Devs.Alyxia, Devs.Remty],
patches: [
{
find: "UserProfileStore",
replacement: {
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
replace: "$self.colorDecodeHook($1)"
},
},
{
find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}",
replacement: {
match: /#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\).+?}\)(?=\])(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
}
}
],
settingsAboutComponent: SettingsAboutComponentWrapper,
settings,
colorDecodeHook(user: User) {
if (user) {
colorDecodeHook(user: UserProfile) {
if (user?.bio) {
// don't replace colors if already set with nitro
if (settings.store.nitroFirst && user.themeColors) return user;
const colors = decode(user.bio);

View file

@ -25,7 +25,7 @@ import { CopyIcon, LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { ConnectedAccount, User } from "@vencord/discord-types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Tooltip, UserProfileStore } from "@webpack/common";
@ -60,15 +60,8 @@ const settings = definePluginSettings({
}
});
interface Connection {
type: string;
id: string;
name: string;
verified: boolean;
}
interface ConnectionPlatform {
getPlatformUserUrl(connection: Connection): string;
getPlatformUserUrl(connection: ConnectedAccount): string;
icon: { lightSVG: string, darkSVG: string; };
}
@ -88,7 +81,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {
if (!profile)
return null;
const connections: Connection[] = profile.connectedAccounts;
const connections = profile.connectedAccounts;
if (!connections?.length)
return null;
@ -102,7 +95,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {
);
}
function CompactConnectionComponent({ connection, theme }: { connection: Connection, theme: string; }) {
function CompactConnectionComponent({ connection, theme }: { connection: ConnectedAccount, theme: string; }) {
const platform = platforms.get(useLegacyPlatformType(connection.type));
const url = platform.getPlatformUserUrl?.(connection);

View file

@ -22,6 +22,7 @@ import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types";
import { ProfileBadge } from "@vencord/discord-types";
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
import { type ComponentType, type ReactNode } from "react";
@ -47,13 +48,6 @@ const badges: Record<string, ProfileBadge> = {
const fetching = new Set<string>();
const queue = new Queue(5);
interface ProfileBadge {
id: string;
description: string;
icon: string;
link?: string;
}
interface MentionProps {
data: {
userId?: string;
@ -102,10 +96,12 @@ async function getUser(id: string) {
// Fill in what we can deduce
const profile = UserProfileStore.getUserProfile(id);
if (profile) {
profile.accentColor = user.accent_color;
profile.badges = fakeBadges;
profile.banner = user.banner;
profile.premiumType = user.premium_type;
}
return userObj;
}

View file

@ -194,7 +194,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
});
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user });
await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body });
await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", userProfile: body });
if (options?.guild_id && body.guild_member)
FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member });

View file

@ -42,7 +42,7 @@ export let GuildStore: t.GuildStore;
export let GuildRoleStore: t.GuildRoleStore;
export let GuildMemberStore: t.GuildMemberStore;
export let UserStore: t.UserStore;
export let UserProfileStore: GenericStore;
export let UserProfileStore: t.UserProfileStore;
export let SelectedChannelStore: t.SelectedChannelStore;
export let SelectedGuildStore: t.SelectedGuildStore;
export let ChannelStore: t.ChannelStore;