Compare commits

..

No commits in common. "hive" and "latest" have entirely different histories.
hive ... latest

238 changed files with 1516 additions and 3088 deletions

View file

@ -1,30 +1,26 @@
# A fork. A fork. We're a fork. # A fork. A fork. We're a fork.
Installing is the same as the Vencord devs laid out: [(Install)](https://docs.vencord.dev/installing/)
* Beyond that, be sure to clone *this* repository instead of their upstream one.
* `git clone https://git.dorkbutt.lol/dorkbutt/vencord`
* After completing the steps, go to Settings > Vencord > Plugins and search for
"FORKED - usrbg" and enable it. Be sure to tweak its settings!
# Vencord # Vencord
![](https://img.shields.io/github/package-json/v/Vendicated/Vencord?style=for-the-badge&logo=github&logoColor=d3869b&label=&color=1d2021&labelColor=282828)
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Vee/cord) [![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Vee/cord)
The cutest Discord client mod The cutest Discord client mod
![](https://github.com/user-attachments/assets/3fac98c0-c411-4d2a-97a3-13b7da8687a2) | ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
| :--------------------------------------------------------------------------------------------------: |
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
## Features ## Features
- Easy to install - Super easy to install (Download Installer, open, click install button, done)
- [100+ built in plugins](https://vencord.dev/plugins) - 100+ plugins built in: [See a list](https://vencord.dev/plugins)
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
- Fairly lightweight despite the many inbuilt plugins - Fairly lightweight despite the many inbuilt plugins
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript - Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work - Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes) - Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
- Privacy friendly: blocks Discord analytics & crash reporting out of the box and has no telemetry - Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
- Maintained very actively, broken plugins are usually fixed within 12 hours - Maintained very actively, broken plugins are usually fixed within 12 hours
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional) - Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)

View file

@ -20,13 +20,16 @@
/// <reference path="../src/globals.d.ts" /> /// <reference path="../src/globals.d.ts" />
import monacoHtmlLocal from "file://monacoWin.html?minify"; import monacoHtmlLocal from "file://monacoWin.html?minify";
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
import * as DataStore from "../src/api/DataStore"; import * as DataStore from "../src/api/DataStore";
import { debounce, localStorage } from "../src/utils"; import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata"; import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
import { getTheme, Theme } from "../src/utils/discord"; import { getTheme, Theme } from "../src/utils/discord";
import { getThemeInfo } from "../src/main/themes"; import { getThemeInfo } from "../src/main/themes";
import { Settings } from "../src/Vencord"; import { Settings } from "../src/Vencord";
import { getStylusWebStoreUrl } from "@utils/web";
// Discord deletes this so need to store in variable
const { localStorage } = window;
// listeners for ipc.on // listeners for ipc.on
const cssListeners = new Set<(css: string) => void>(); const cssListeners = new Set<(css: string) => void>();
@ -42,13 +45,12 @@ window.VencordNative = {
themes: { themes: {
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore), uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore), deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
getThemesDir: async () => "",
getThemesList: () => DataStore.entries(themeStore).then(entries => getThemesList: () => DataStore.entries(themeStore).then(entries =>
entries.map(([name, css]) => getThemeInfo(css, name.toString())) entries.map(([name, css]) => getThemeInfo(css, name.toString()))
), ),
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore), getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),
getSystemValues: async () => ({}), getSystemValues: async () => ({}),
openFolder: async () => Promise.reject("themes:openFolder is not supported on web"),
}, },
native: { native: {
@ -75,14 +77,6 @@ window.VencordNative = {
addThemeChangeListener: NOOP, addThemeChangeListener: NOOP,
openFile: NOOP_ASYNC, openFile: NOOP_ASYNC,
async openEditor() { async openEditor() {
if (IS_USERSCRIPT) {
const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?");
if (shouldOpenWebStore) {
window.open(getStylusWebStoreUrl(), "_blank");
}
return;
}
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`; const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features); const win = open("about:blank", "VencordQuickCss", features);
if (!win) { if (!win) {
@ -98,7 +92,7 @@ window.VencordNative = {
? "vs-light" ? "vs-light"
: "vs-dark"; : "vs-dark";
win.document.write(monacoHtmlLocal); win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
}, },
}, },
@ -112,9 +106,8 @@ window.VencordNative = {
} }
}, },
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)), set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
openFolder: async () => Promise.reject("settings:openFolder is not supported on web"), getSettingsDir: async () => "LocalStorage"
}, },
pluginHelpers: {} as any, pluginHelpers: {} as any,
csp: {} as any,
}; };

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.12.6", "version": "1.12.2",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -53,8 +53,8 @@
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5", "@types/yazl": "^2.4.5",
"@vencord/discord-types": "link:packages/discord-types",
"diff": "^7.0.0", "diff": "^7.0.0",
"discord-types": "^1.3.26",
"esbuild": "^0.25.1", "esbuild": "^0.25.1",
"eslint": "9.20.1", "eslint": "9.20.1",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",

View file

@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -1,42 +0,0 @@
# Discord Types
This package provides TypeScript types for the Webpack modules of Discord's web app.
While it was primarily created for Vencord, other client mods could also benefit from this, so it is published as a standalone package!
## Installation
```bash
npm install -D @vencord/discord-types
yarn add -D @vencord/discord-types
pnpm add -D @vencord/discord-types
```
## Example Usage
```ts
import type { UserStore } from "@vencord/discord-types";
const userStore: UserStore = findStore("UserStore"); // findStore is up to you to implement, this library only provides types and no runtime code
```
## Enums
This library also exports some const enums that you can use from Typescript code:
```ts
import { ApplicationCommandType } from "@vencord/discord-types/enums";
console.log(ApplicationCommandType.CHAT_INPUT); // 1
```
### License
This package is licensed under the [LGPL-3.0](./LICENSE) (or later) license.
A very short summary of the license is that you can use this package as a library in both open source and closed source projects,
similar to an MIT-licensed project.
However, if you modify the code of this package, you must release source code of your modified version under the same license.
### Credit
This package was inspired by Swishilicous' [discord-types](https://www.npmjs.com/package/discord-types) package.

View file

@ -1,32 +0,0 @@
export const enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export const enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export const enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export const enum ApplicationIntegrationType {
GUILD_INSTALL = 0,
USER_INSTALL = 1
}

View file

@ -1 +0,0 @@
export * from "./commands";

View file

@ -1,19 +0,0 @@
{
"name": "@vencord/discord-types",
"author": "Vencord Contributors",
"description": "Typescript definitions for the webpack modules of the Discord Web app",
"version": "1.0.0",
"license": "LGPL-3.0-or-later",
"types": "src/index.d.ts",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/Vendicated/Vencord.git",
"directory": "packages/discord-types"
},
"dependencies": {
"@types/react": "^19.0.10",
"moment": "^2.22.2",
"type-fest": "^4.41.0"
}
}

View file

@ -1,21 +0,0 @@
export interface ImageModalClasses {
image: string,
modal: string,
}
export interface ButtonWrapperClasses {
hoverScale: string;
buttonWrapper: string;
button: string;
iconMask: string;
buttonContent: string;
icon: string;
pulseIcon: string;
pulseButton: string;
notificationDot: string;
sparkleContainer: string;
sparkleStar: string;
sparklePlus: string;
sparkle: string;
active: string;
}

View file

@ -1,23 +0,0 @@
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

@ -1,83 +0,0 @@
import { DiscordRecord } from "./Record";
export class Channel extends DiscordRecord {
constructor(channel: object);
application_id: number | undefined;
bitrate: number;
defaultAutoArchiveDuration: number | undefined;
flags: number;
guild_id: string;
icon: string;
id: string;
lastMessageId: string;
lastPinTimestamp: string | undefined;
member: unknown;
memberCount: number | undefined;
memberIdsPreview: string[] | undefined;
memberListId: unknown;
messageCount: number | undefined;
name: string;
nicks: Record<string, unknown>;
nsfw: boolean;
originChannelId: unknown;
ownerId: string;
parent_id: string;
permissionOverwrites: {
[role: string]: {
id: string;
type: number;
deny: bigint;
allow: bigint;
};
};
position: number;
rateLimitPerUser: number;
rawRecipients: {
id: string;
avatar: string;
username: string;
public_flags: number;
discriminator: string;
}[];
recipients: string[];
rtcRegion: string;
threadMetadata: {
locked: boolean;
archived: boolean;
invitable: boolean;
createTimestamp: string | undefined;
autoArchiveDuration: number;
archiveTimestamp: string | undefined;
};
topic: string;
type: number;
userLimit: number;
videoQualityMode: undefined;
get accessPermissions(): bigint;
get lastActiveTimestamp(): number;
computeLurkerPermissionsAllowList(): unknown;
getApplicationId(): unknown;
getGuildId(): string;
getRecipientId(): unknown;
hasFlag(flag: number): boolean;
isActiveThread(): boolean;
isArchivedThread(): boolean;
isCategory(): boolean;
isDM(): boolean;
isDirectory(): boolean;
isForumChannel(): boolean;
isGroupDM(): boolean;
isGuildStageVoice(): boolean;
isGuildVoice(): boolean;
isListenModeCapable(): boolean;
isManaged(): boolean;
isMultiUserDM(): boolean;
isNSFW(): boolean;
isOwner(): boolean;
isPrivate(): boolean;
isSystemDM(): boolean;
isThread(): boolean;
isVocal(): boolean;
}

View file

@ -1,64 +0,0 @@
import { Role } from './Role';
import { DiscordRecord } from './Record';
// copy(Object.keys(findByProps("CREATOR_MONETIZABLE")).map(JSON.stringify).join("|"))
export type GuildFeatures =
"INVITE_SPLASH" | "VIP_REGIONS" | "VANITY_URL" | "MORE_EMOJI" | "MORE_STICKERS" | "MORE_SOUNDBOARD" | "VERIFIED" | "COMMERCE" | "DISCOVERABLE" | "COMMUNITY" | "FEATURABLE" | "NEWS" | "HUB" | "PARTNERED" | "ANIMATED_ICON" | "BANNER" | "ENABLED_DISCOVERABLE_BEFORE" | "WELCOME_SCREEN_ENABLED" | "MEMBER_VERIFICATION_GATE_ENABLED" | "PREVIEW_ENABLED" | "ROLE_SUBSCRIPTIONS_ENABLED" | "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" | "CREATOR_MONETIZABLE" | "CREATOR_MONETIZABLE_PROVISIONAL" | "CREATOR_MONETIZABLE_WHITEGLOVE" | "CREATOR_MONETIZABLE_DISABLED" | "CREATOR_MONETIZABLE_RESTRICTED" | "CREATOR_STORE_PAGE" | "CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING" | "PRODUCTS_AVAILABLE_FOR_PURCHASE" | "GUILD_WEB_PAGE_VANITY_URL" | "THREADS_ENABLED" | "THREADS_ENABLED_TESTING" | "NEW_THREAD_PERMISSIONS" | "ROLE_ICONS" | "TEXT_IN_STAGE_ENABLED" | "TEXT_IN_VOICE_ENABLED" | "HAS_DIRECTORY_ENTRY" | "ANIMATED_BANNER" | "LINKED_TO_HUB" | "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT" | "GUILD_HOME_DEPRECATION_OVERRIDE" | "GUILD_HOME_TEST" | "GUILD_HOME_OVERRIDE" | "GUILD_ONBOARDING" | "GUILD_ONBOARDING_EVER_ENABLED" | "GUILD_ONBOARDING_HAS_PROMPTS" | "GUILD_SERVER_GUIDE" | "INTERNAL_EMPLOYEE_ONLY" | "AUTO_MODERATION" | "INVITES_DISABLED" | "BURST_REACTIONS" | "SOUNDBOARD" | "SHARD" | "ACTIVITY_FEED_ENABLED_BY_USER" | "ACTIVITY_FEED_DISABLED_BY_USER" | "SUMMARIES_ENABLED_GA" | "LEADERBOARD_ENABLED" | "SUMMARIES_ENABLED_BY_USER" | "SUMMARIES_OPT_OUT_EXPERIENCE" | "CHANNEL_ICON_EMOJIS_GENERATED" | "NON_COMMUNITY_RAID_ALERTS" | "RAID_ALERTS_DISABLED" | "AUTOMOD_TRIGGER_USER_PROFILE" | "ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY" | "GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE" | "CLAN" | "MEMBER_VERIFICATION_MANUAL_APPROVAL" | "FORWARDING_DISABLED" | "MEMBER_VERIFICATION_ROLLOUT_TEST" | "AUDIO_BITRATE_128_KBPS" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS" | "VIDEO_BITRATE_ENHANCED" | "MAX_FILE_SIZE_50_MB" | "MAX_FILE_SIZE_100_MB" | "GUILD_TAGS" | "ENHANCED_ROLE_COLORS" | "PREMIUM_TIER_3_OVERRIDE" | "REPORT_TO_MOD_PILOT" | "TIERLESS_BOOSTING_SYSTEM_MESSAGE";
export type GuildPremiumFeatures =
"ANIMATED_ICON" | "STAGE_CHANNEL_VIEWERS_150" | "ROLE_ICONS" | "GUILD_TAGS" | "BANNER" | "MAX_FILE_SIZE_50_MB" | "VIDEO_QUALITY_720_60FPS" | "STAGE_CHANNEL_VIEWERS_50" | "VIDEO_QUALITY_1080_60FPS" | "MAX_FILE_SIZE_100_MB" | "VANITY_URL" | "VIDEO_BITRATE_ENHANCED" | "STAGE_CHANNEL_VIEWERS_300" | "AUDIO_BITRATE_128_KBPS" | "ANIMATED_BANNER" | "TIERLESS_BOOSTING" | "ENHANCED_ROLE_COLORS" | "INVITE_SPLASH" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS";
export class Guild extends DiscordRecord {
constructor(guild: object);
afkChannelId: string | undefined;
afkTimeout: number;
applicationCommandCounts: {
0: number;
1: number;
2: number;
};
application_id: unknown;
banner: string | undefined;
defaultMessageNotifications: number;
description: string | undefined;
discoverySplash: string | undefined;
explicitContentFilter: number;
features: Set<GuildFeatures>;
homeHeader: string | undefined;
hubType: unknown;
icon: string | undefined;
id: string;
joinedAt: Date;
latestOnboardingQuestionId: string | undefined;
maxMembers: number;
maxStageVideoChannelUsers: number;
maxVideoChannelUsers: number;
mfaLevel: number;
moderatorReporting: unknown;
name: string;
nsfwLevel: number;
ownerConfiguredContentLevel: number;
ownerId: string;
preferredLocale: string;
premiumFeatures: {
additionalEmojiSlots: number;
additionalSoundSlots: number;
additionalStickerSlots: number;
features: Array<GuildPremiumFeatures>;
};
premiumProgressBarEnabled: boolean;
premiumSubscriberCount: number;
premiumTier: number;
profile: {
badge: string | undefined;
tag: string | undefined;
} | undefined;
publicUpdatesChannelId: string | undefined;
roles: Record<string, Role>;
rulesChannelId: string | undefined;
safetyAlertsChannelId: string | undefined;
splash: string | undefined;
systemChannelFlags: number;
systemChannelId: string | undefined;
vanityURLCode: string | undefined;
verificationLevel: number;
}

View file

@ -1,26 +0,0 @@
export interface GuildMember {
avatar: string | undefined;
avatarDecoration: string | undefined;
banner: string | undefined;
bio: string;
colorRoleId: string | undefined;
colorString: string;
colorStrings: {
primaryColor: string | undefined;
secondaryColor: string | undefined;
tertiaryColor: string | undefined;
};
communicationDisabledUntil: string | undefined;
flags: number;
fullProfileLoadedTimestamp: number;
guildId: string;
highestRoleId: string;
hoistRoleId: string;
iconRoleId: string;
isPending: boolean | undefined;
joinedAt: string | undefined;
nick: string | undefined;
premiumSince: string | undefined;
roles: string[];
userId: string;
}

View file

@ -1,12 +0,0 @@
type Updater = (value: any) => any;
/**
* Common Record class extended by various Discord data structures, like User, Channel, Guild, etc.
*/
export class DiscordRecord {
toJS(): Record<string, any>;
set(key: string, value: any): this;
merge(data: Record<string, any>): this;
update(key: string, defaultValueOrUpdater: Updater | any, updater?: Updater): this;
}

View file

@ -1,33 +0,0 @@
export interface Role {
color: number;
colorString: string | undefined;
colorStrings: {
primaryColor: string | undefined;
secondaryColor: string | undefined;
tertiaryColor: string | undefined;
};
colors: {
primary_color: number | undefined;
secondary_color: number | undefined;
tertiary_color: number | undefined;
};
flags: number;
hoist: boolean;
icon: string | undefined;
id: string;
managed: boolean;
mentionable: boolean;
name: string;
originalPosition: number;
permissions: bigint;
position: number;
/**
* probably incomplete
*/
tags: {
bot_id: string;
integration_id: string;
premium_subscriber: unknown;
} | undefined;
unicodeEmoji: string | undefined;
}

View file

@ -1,65 +0,0 @@
// TODO: a lot of optional params can also be null, not just undef
import { DiscordRecord } from "./Record";
export class User extends DiscordRecord {
constructor(user: object);
accentColor: number;
avatar: string;
banner: string | null | undefined;
bio: string;
bot: boolean;
desktop: boolean;
discriminator: string;
email: string | undefined;
flags: number;
globalName: string | undefined;
guildMemberAvatars: Record<string, string>;
id: string;
mfaEnabled: boolean;
mobile: boolean;
nsfwAllowed: boolean | undefined;
phone: string | undefined;
premiumType: number | undefined;
premiumUsageFlags: number;
publicFlags: number;
purchasedFlags: number;
system: boolean;
username: string;
verified: boolean;
get createdAt(): Date;
get hasPremiumPerks(): boolean;
get tag(): string;
get usernameNormalized(): string;
addGuildAvatarHash(guildId: string, avatarHash: string): User;
getAvatarSource(guildId: string, canAnimate?: boolean): { uri: string; };
getAvatarURL(guildId?: string | null, t?: unknown, canAnimate?: boolean): string;
hasAvatarForGuild(guildId: string): boolean;
hasDisabledPremium(): boolean;
hasFlag(flag: number): boolean;
hasFreePremium(): boolean;
hasHadSKU(e: unknown): boolean;
hasPremiumUsageFlag(flag: number): boolean;
hasPurchasedFlag(flag: number): boolean;
hasUrgentMessages(): boolean;
isClaimed(): boolean;
isLocalBot(): boolean;
isNonUserBot(): boolean;
isPhoneVerified(): boolean;
isStaff(): boolean;
isSystemUser(): boolean;
isVerifiedBot(): boolean;
removeGuildAvatarHash(guildId: string): User;
toString(): string;
}
export interface UserJSON {
avatar: string;
avatarDecoration: unknown | undefined;
discriminator: string;
id: string;
publicFlags: number;
username: string;
}

View file

@ -1,8 +0,0 @@
export * from "./Application";
export * from "./Channel";
export * from "./Guild";
export * from "./GuildMember";
export * from "./messages";
export * from "./Role";
export * from "./User";
export * from "./Record";

View file

@ -1,61 +0,0 @@
import { Channel } from "../Channel";
import { Guild } from "../Guild";
import { Promisable } from "type-fest";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from "../../../enums";
export interface CommandContext {
channel: Channel;
guild?: Guild;
}
export interface CommandOption {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: CommandOption[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export interface CommandReturnValue {
content: string;
// TODO: implement
// cancel?: boolean;
}
export interface CommandArgument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
options: CommandArgument[];
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: CommandOption[];
predicate?(ctx: CommandContext): boolean;
execute(args: CommandArgument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
}

View file

@ -1,70 +0,0 @@
export interface Embed {
author?: {
name: string;
url: string;
iconURL: string | undefined;
iconProxyURL: string | undefined;
};
color: string;
fields: [];
id: string;
image?: {
height: number;
width: number;
url: string;
proxyURL: string;
};
provider?: {
name: string;
url: string | undefined;
};
rawDescription: string;
rawTitle: string;
referenceId: unknown;
timestamp: string;
thumbnail?: {
height: number;
proxyURL: string | undefined;
url: string;
width: number;
};
type: string;
url: string | undefined;
video?: {
height: number;
width: number;
url: string;
proxyURL: string | undefined;
};
}
export interface EmbedJSON {
author?: {
name: string;
url: string;
icon_url: string;
proxy_icon_url: string;
};
title: string;
color: string;
description: string;
type: string;
url: string | undefined;
provider?: {
name: string;
url: string;
};
timestamp: string;
thumbnail?: {
height: number;
width: number;
url: string;
proxy_url: string | undefined;
};
video?: {
height: number;
width: number;
url: string;
proxy_url: string | undefined;
};
}

View file

@ -1,42 +0,0 @@
export type Emoji = CustomEmoji | UnicodeEmoji;
export interface CustomEmoji {
type: 1;
allNamesString: string;
animated: boolean;
available: boolean;
guildId: string;
id: string;
managed: boolean;
name: string;
originalName?: string;
require_colons: boolean;
roles: string[];
}
export interface UnicodeEmoji {
type: 0;
diversityChildren: Record<any, any>;
emojiObject: {
names: string[];
surrogates: string;
unicodeVersion: number;
};
index: number;
surrogates: string;
uniqueName: string;
useSpriteSheet: boolean;
get allNamesString(): string;
get animated(): boolean;
get defaultDiversityChild(): any;
get hasDiversity(): boolean | undefined;
get hasDiversityParent(): boolean | undefined;
get hasMultiDiversity(): boolean | undefined;
get hasMultiDiversityParent(): boolean | undefined;
get managed(): boolean;
get name(): string;
get names(): string[];
get optionallyDiverseSequence(): string | undefined;
get unicodeVersion(): number;
get url(): string;
}

View file

@ -1,191 +0,0 @@
import { CommandOption } from './Commands';
import { User, UserJSON } from '../User';
import { Embed, EmbedJSON } from './Embed';
import { DiscordRecord } from "../Record";
/**
* TODO: looks like discord has moved over to Date instead of Moment;
*/
export class Message extends DiscordRecord {
constructor(message: object);
activity: unknown;
application: unknown;
applicationId: string | unknown;
attachments: MessageAttachment[];
author: User;
blocked: boolean;
bot: boolean;
call: {
duration: moment.Duration;
endedTimestamp: moment.Moment;
participants: string[];
};
channel_id: string;
/**
* NOTE: not fully typed
*/
codedLinks: {
code?: string;
type: string;
}[];
colorString: unknown;
components: unknown[];
content: string;
customRenderedContent: unknown;
editedTimestamp: Date;
embeds: Embed[];
flags: number;
giftCodes: string[];
id: string;
interaction: {
id: string;
name: string;
type: number;
user: User;
}[] | undefined;
interactionData: {
application_command: {
application_id: string;
default_member_permissions: unknown;
default_permission: boolean;
description: string;
dm_permission: unknown;
id: string;
name: string;
options: CommandOption[];
permissions: unknown[];
type: number;
version: string;
};
attachments: MessageAttachment[];
guild_id: string | undefined;
id: string;
name: string;
options: {
focused: unknown;
name: string;
type: number;
value: string;
}[];
type: number;
version: string;
}[];
interactionError: unknown[];
isSearchHit: boolean;
loggingName: unknown;
mentionChannels: string[];
mentionEveryone: boolean;
mentionRoles: string[];
mentioned: boolean;
mentions: string[];
messageReference: {
guild_id?: string;
channel_id: string;
message_id: string;
} | undefined;
nick: unknown; // probably a string
nonce: string | undefined;
pinned: boolean;
reactions: MessageReaction[];
state: string;
stickerItems: {
format_type: number;
id: string;
name: string;
}[];
stickers: unknown[];
timestamp: moment.Moment;
tts: boolean;
type: number;
webhookId: string | undefined;
/**
* Doesn't actually update the original message; it just returns a new message instance with the added reaction.
*/
addReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
/**
* Searches each reaction and if the provided string has an index above -1 it'll return the reaction object.
*/
getReaction(name: string): MessageReaction;
/**
* Doesn't actually update the original message; it just returns the message instance without the reaction searched with the provided emoji object.
*/
removeReactionsForEmoji(emoji: ReactionEmoji): Message;
/**
* Doesn't actually update the original message; it just returns the message instance without the reaction.
*/
removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
getChannelId(): string;
hasFlag(flag: number): boolean;
isCommandType(): boolean;
isEdited(): boolean;
isSystemDM(): boolean;
}
/** A smaller Message object found in FluxDispatcher and elsewhere. */
export interface MessageJSON {
attachments: MessageAttachment[];
author: UserJSON;
channel_id: string;
components: unknown[];
content: string;
edited_timestamp: string;
embeds: EmbedJSON[];
flags: number;
guild_id: string | undefined;
id: string;
loggingName: unknown;
member: {
avatar: string | undefined;
communication_disabled_until: string | undefined;
deaf: boolean;
hoisted_role: string | undefined;
is_pending: boolean;
joined_at: string;
mute: boolean;
nick: string | boolean;
pending: boolean;
premium_since: string | undefined;
roles: string[];
} | undefined;
mention_everyone: boolean;
mention_roles: string[];
mentions: UserJSON[];
message_reference: {
guild_id?: string;
channel_id: string;
message_id: string;
} | undefined;
nonce: string | undefined;
pinned: boolean;
referenced_message: MessageJSON | undefined;
state: string;
timestamp: string;
tts: boolean;
type: number;
}
export interface MessageAttachment {
filename: string;
id: string;
proxy_url: string;
size: number;
spoiler: boolean;
url: string;
content_type?: string;
width?: number;
height?: number;
}
export interface ReactionEmoji {
id: string | undefined;
name: string;
animated: boolean;
}
export interface MessageReaction {
count: number;
emoji: ReactionEmoji;
me: boolean;
}

View file

@ -1,4 +0,0 @@
export * from "./Commands";
export * from "./Message";
export * from "./Embed";
export * from "./Emoji";

View file

@ -1,30 +0,0 @@
import { FluxStore } from "./stores/FluxStore";
export class FluxEmitter {
constructor();
changeSentinel: number;
changedStores: Set<FluxStore>;
isBatchEmitting: boolean;
isDispatching: boolean;
isPaused: boolean;
pauseTimer: NodeJS.Timeout | null;
reactChangedStores: Set<FluxStore>;
batched(batch: (...args: any[]) => void): void;
destroy(): void;
emit(): void;
emitNonReactOnce(): void;
emitReactOnce(): void;
getChangeSentinel(): number;
getIsPaused(): boolean;
injectBatchEmitChanges(batch: (...args: any[]) => void): void;
markChanged(store: FluxStore): void;
pause(): void;
resume(): void;
}
export interface Flux {
Store: typeof FluxStore;
Emitter: FluxEmitter;
}

File diff suppressed because one or more lines are too long

View file

@ -1,9 +0,0 @@
export * from "./common";
export * from "./classes";
export * from "./components";
export * from "./flux";
export * from "./fluxEvents";
export * from "./menu";
export * from "./stores";
export * from "./utils";
export * as Webpack from "../webpack";

View file

@ -1,24 +0,0 @@
import { Channel, FluxStore } from "..";
export class ChannelStore extends FluxStore {
getChannel(channelId: string): Channel;
getBasicChannel(channelId: string): Channel | undefined;
hasChannel(channelId: string): boolean;
getChannelIds(guildId?: string | null): string[];
getMutableBasicGuildChannelsForGuild(guildId: string): Record<string, Channel>;
getMutableGuildChannelsForGuild(guildId: string): Record<string, Channel>;
getAllThreadsForGuild(guildId: string): Channel[];
getAllThreadsForParent(channelId: string): Channel[];
getDMFromUserId(userId: string): string;
getDMChannelFromUserId(userId: string): Channel | undefined;
getDMUserIds(): string[];
getMutableDMsByUserIds(): Record<string, string>;
getMutablePrivateChannels(): Record<string, Channel>;
getSortedPrivateChannels(): Channel[];
getGuildChannelsVersion(guildId: string): number;
getPrivateChannelsVersion(): number;
getInitialOverlayState(): Record<string, Channel>;
}

View file

@ -1,43 +0,0 @@
import { FluxStore } from "..";
export enum DraftType {
ChannelMessage = 0,
ThreadSettings = 1,
FirstThreadMessage = 2,
ApplicationLauncherCommand = 3,
Poll = 4,
SlashCommand = 5,
ForwardContextMessage = 6
}
export interface Draft {
timestamp: number;
draft: string;
}
export interface ThreadSettingsDraft {
timestamp: number;
parentMessageId?: string;
name?: string;
isPrivate?: boolean;
parentChannelId?: string;
location?: string;
}
export type ChannelDrafts = {
[DraftType.ThreadSettings]: ThreadSettingsDraft;
} & {
[key in Exclude<DraftType, DraftType.ThreadSettings>]: Draft;
};
export type UserDrafts = Partial<Record<string, ChannelDrafts>>;
export type DraftState = Partial<Record<string, UserDrafts>>;
export class DraftStore extends FluxStore {
getState(): DraftState;
getRecentlyEditedDrafts(type: DraftType): Array<Draft & { channelId: string; }>;
getDraft(channelId: string, type: DraftType): string;
getThreadSettings(channelId: string): ThreadSettingsDraft | null | undefined;
getThreadDraftWithParentMessageId(parentMessageId: string): ThreadSettingsDraft | null | undefined;
}

View file

@ -1,57 +0,0 @@
import { Channel, CustomEmoji, Emoji, FluxStore } from "..";
export class EmojiStore extends FluxStore {
getCustomEmojiById(id?: string | null): CustomEmoji | undefined;
getUsableCustomEmojiById(id?: string | null): CustomEmoji | undefined;
getGuilds(): Record<string, {
id: string;
get emojis(): CustomEmoji[];
get rawEmojis(): CustomEmoji[];
get usableEmojis(): CustomEmoji[];
get emoticons(): any[];
getEmoji(id: string): CustomEmoji | undefined;
isUsable(emoji: CustomEmoji): boolean;
}>;
getGuildEmoji(guildId?: string | null): CustomEmoji[];
getNewlyAddedEmoji(guildId?: string | null): CustomEmoji[];
getTopEmoji(guildId?: string | null): CustomEmoji[];
getTopEmojisMetadata(guildId?: string | null): {
emojiIds: string[];
topEmojisTTL: number;
};
hasPendingUsage(): boolean;
hasUsableEmojiInAnyGuild(): boolean;
searchWithoutFetchingLatest(data: any): any;
getSearchResultsOrder(...args: any[]): any;
getState(): {
pendingUsages: { key: string, timestamp: number; }[];
};
searchWithoutFetchingLatest(data: {
channel: Channel;
query: string;
count?: number;
intention: number;
includeExternalGuilds?: boolean;
matchComparator?(name: string): boolean;
}): Record<"locked" | "unlocked", Emoji[]>;
getDisambiguatedEmojiContext(): {
backfillTopEmojis: Record<any, any>;
customEmojis: Record<string, CustomEmoji>;
emojisById: Record<string, CustomEmoji>;
emojisByName: Record<string, CustomEmoji>;
emoticonRegex: RegExp | null;
emoticonsByName: Record<string, any>;
escapedEmoticonNames: string;
favoriteNamesAndIds?: any;
favorites?: any;
frequentlyUsed?: any;
groupedCustomEmojis: Record<string, CustomEmoji[]>;
guildId?: string;
isFavoriteEmojiWithoutFetchingLatest(e: Emoji): boolean;
newlyAddedEmoji: Record<string, CustomEmoji[]>;
topEmojis?: any;
unicodeAliases: Record<string, string>;
get favoriteEmojisWithoutFetchingLatest(): Emoji[];
};
}

View file

@ -1,44 +0,0 @@
import { FluxDispatcher, FluxEvents } from "..";
type Callback = () => void;
/*
For some reason, this causes type errors when you try to destructure it:
```ts
interface FluxEvent {
type: FluxEvents;
[key: string]: any;
}
```
*/
export type FluxEvent = any;
export type ActionHandler = (event: FluxEvent) => void;
export type ActionHandlers = Partial<Record<FluxEvents, ActionHandler>>;
export class FluxStore {
constructor(dispatcher: FluxDispatcher, actionHandlers?: ActionHandlers);
getName(): string;
addChangeListener(callback: Callback): void;
/** Listener will be removed once the callback returns false. */
addConditionalChangeListener(callback: () => boolean, preemptive?: boolean): void;
addReactChangeListener(callback: Callback): void;
removeChangeListener(callback: Callback): void;
removeReactChangeListener(callback: Callback): void;
doEmitChanges(event: FluxEvent): void;
emitChange(): void;
getDispatchToken(): string;
initialize(): void;
initializeIfNeeded(): void;
/** this is a setter */
mustEmitChanges(actionHandler: ActionHandler | undefined): void;
registerActionHandlers(actionHandlers: ActionHandlers): void;
syncWith(stores: FluxStore[], callback: Callback, timeout?: number): void;
waitFor(...stores: FluxStore[]): void;
static getAll(): FluxStore[];
}

View file

@ -1,27 +0,0 @@
import { FluxStore, GuildMember } from "..";
export class GuildMemberStore extends FluxStore {
/** @returns Format: [guildId-userId: Timestamp (string)] */
getCommunicationDisabledUserMap(): Record<string, string>;
getCommunicationDisabledVersion(): number;
getMutableAllGuildsAndMembers(): Record<string, Record<string, GuildMember>>;
getMember(guildId: string, userId: string): GuildMember | null;
getTrueMember(guildId: string, userId: string): GuildMember | null;
getMemberIds(guildId: string): string[];
getMembers(guildId: string): GuildMember[];
getCachedSelfMember(guildId: string): GuildMember | null;
getSelfMember(guildId: string): GuildMember | null;
getSelfMemberJoinedAt(guildId: string): Date | null;
getNick(guildId: string, userId: string): string | null;
getNicknameGuildsMapping(userId: string): Record<string, string[]>;
getNicknames(userId: string): string[];
isMember(guildId: string, userId: string): boolean;
isMember(guildId: string, userId: string): boolean;
isGuestOrLurker(guildId: string, userId: string): boolean;
isCurrentUserGuest(guildId: string): boolean;
}

View file

@ -1,7 +0,0 @@
import { FluxStore, Role } from "..";
export class GuildRoleStore extends FluxStore {
getRole(guildId: string, roleId: string): Role;
getRoles(guildId: string): Record<string, Role>;
getAllGuildRoles(): Record<string, Record<string, Role>>;
}

View file

@ -1,8 +0,0 @@
import { Guild, FluxStore } from "..";
export class GuildStore extends FluxStore {
getGuild(guildId: string): Guild;
getGuildCount(): number;
getGuilds(): Record<string, Guild>;
getGuildIds(): string[];
}

View file

@ -1,13 +0,0 @@
import { MessageJSON, FluxStore, Message } from "..";
export class MessageStore extends FluxStore {
getMessage(channelId: string, messageId: string): Message;
/** @returns This return object is fucking huge; I'll type it later. */
getMessages(channelId: string): unknown;
getRawMessages(channelId: string): Record<string | number, MessageJSON>;
hasCurrentUserSentMessage(channelId: string): boolean;
hasPresent(channelId: string): boolean;
isLoadingMessages(channelId: string): boolean;
jumpedMessageId(channelId: string): string | undefined;
whenReady(channelId: string, callback: () => void): void;
}

View file

@ -1,21 +0,0 @@
import { FluxStore } from "..";
export class RelationshipStore extends FluxStore {
getFriendIDs(): string[];
getIgnoredIDs(): string[];
getBlockedIDs(): string[];
getPendingCount(): number;
getRelationshipCount(): number;
/** Related to friend nicknames. */
getNickname(userId: string): string;
/** @returns Enum value from constants.RelationshipTypes */
getRelationshipType(userId: string): number;
isFriend(userId: string): boolean;
isBlocked(userId: string): boolean;
isIgnored(userId: string): boolean;
getSince(userId: string): string;
getMutableRelationships(): Map<string, number>;
}

View file

@ -1,14 +0,0 @@
import { FluxStore } from "..";
export class SelectedChannelStore extends FluxStore {
getChannelId(guildId?: string | null): string;
getVoiceChannelId(): string | undefined;
getCurrentlySelectedChannelId(guildId?: string): string | undefined;
getMostRecentSelectedTextChannelId(guildId: string): string | undefined;
getLastSelectedChannelId(guildId?: string): string;
// yes this returns a string
getLastSelectedChannels(guildId?: string): string;
/** If you follow an announcement channel, this will return whichever channel you chose as destination */
getLastChannelFollowingDestination(): { guildId?: string; channelId?: string; } | undefined;
}

View file

@ -1,14 +0,0 @@
import { FluxStore } from "..";
export interface SelectedGuildState {
selectedGuildTimestampMillis: Record<string | number, number>;
selectedGuildId: string | null;
lastSelectedGuildId: string | null;
}
export class SelectedGuildStore extends FluxStore {
getGuildId(): string | null;
getLastSelectedGuildId(): string | null;
getLastSelectedTimestamp(guildId: string): number | null;
getState(): SelectedGuildState | undefined;
}

View file

@ -1,18 +0,0 @@
import { FluxStore } from "..";
export type ThemePreference = "dark" | "light" | "unknown";
export type SystemTheme = "dark" | "light";
export type Theme = "light" | "dark" | "darker" | "midnight";
export interface ThemeState {
theme: Theme;
status: 0 | 1;
preferences: Record<ThemePreference, Theme>;
}
export class ThemeStore extends FluxStore {
get theme(): Theme;
get darkSidebar(): boolean;
get systemTheme(): SystemTheme;
themePreferenceForSystemTheme(preference: ThemePreference): Theme;
getState(): ThemeState;
}

View file

@ -1,151 +0,0 @@
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

@ -1,10 +0,0 @@
import { FluxStore, User } from "..";
export class UserStore extends FluxStore {
filter(filter: (user: User) => boolean, sort?: boolean): Record<string, User>;
findByTag(username: string, discriminator: string): User;
forEach(action: (user: User) => void): void;
getCurrentUser(): User;
getUser(userId: string): User;
getUsers(): Record<string, User>;
}

View file

@ -1,7 +0,0 @@
import { FluxStore } from "..";
export class WindowStore extends FluxStore {
isElementFullScreen(): boolean;
isFocused(): boolean;
windowSize(): Record<"width" | "height", number>;
}

View file

@ -1,33 +0,0 @@
// please keep in alphabetical order
export * from "./ChannelStore";
export * from "./DraftStore";
export * from "./EmojiStore";
export * from "./FluxStore";
export * from "./GuildMemberStore";
export * from "./GuildRoleStore";
export * from "./GuildStore";
export * from "./MessageStore";
export * from "./RelationshipStore";
export * from "./SelectedChannelStore";
export * from "./SelectedGuildStore";
export * from "./ThemeStore";
export * from "./UserProfileStore";
export * from "./UserStore";
export * from "./WindowStore";
/**
* React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object
* @param stores The stores to listen to
* @param mapper A function that returns the data you need
* @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
* @param isEqual A custom comparator for the data returned by mapper
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
export type useStateFromStores = <T>(
stores: any[],
mapper: () => T,
dependencies?: any,
isEqual?: (old: T, newer: T) => boolean
) => T;

View file

@ -21,6 +21,7 @@
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@types/react": "18.3.1", "@types/react": "18.3.1",
"@types/react-dom": "18.3.1", "@types/react-dom": "18.3.1",
"discord-types": "^1.3.26",
"standalone-electron-types": "^34.2.0", "standalone-electron-types": "^34.2.0",
"type-fest": "^4.35.0" "type-fest": "^4.35.0"
} }

43
pnpm-lock.yaml generated
View file

@ -65,12 +65,12 @@ importers:
'@types/yazl': '@types/yazl':
specifier: ^2.4.5 specifier: ^2.4.5
version: 2.4.6 version: 2.4.6
'@vencord/discord-types':
specifier: link:packages/discord-types
version: link:packages/discord-types
diff: diff:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0 version: 7.0.0
discord-types:
specifier: ^1.3.26
version: 1.3.26
esbuild: esbuild:
specifier: ^0.25.1 specifier: ^0.25.1
version: 0.25.1 version: 0.25.1
@ -141,18 +141,6 @@ importers:
specifier: ^0.3.5 specifier: ^0.3.5
version: 0.3.5 version: 0.3.5
packages/discord-types:
dependencies:
'@types/react':
specifier: ^19.0.10
version: 19.0.12
moment:
specifier: ^2.22.2
version: 2.30.1
type-fest:
specifier: ^4.41.0
version: 4.41.0
packages/vencord-types: packages/vencord-types:
dependencies: dependencies:
'@types/lodash': '@types/lodash':
@ -167,6 +155,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: 18.3.1 specifier: 18.3.1
version: 18.3.1 version: 18.3.1
discord-types:
specifier: ^1.3.26
version: 1.3.26
standalone-electron-types: standalone-electron-types:
specifier: ^34.2.0 specifier: ^34.2.0
version: 34.2.0 version: 34.2.0
@ -531,6 +522,9 @@ packages:
peerDependencies: peerDependencies:
'@types/react': ^19.0.0 '@types/react': ^19.0.0
'@types/react@17.0.2':
resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==}
'@types/react@18.3.1': '@types/react@18.3.1':
resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==}
@ -962,6 +956,9 @@ packages:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'} engines: {node: '>=8'}
discord-types@1.3.26:
resolution: {integrity: sha512-ToG51AOCH+JTQf7b+8vuYQe5Iqwz7nZ7StpECAZ/VZcI1ZhQk13pvt9KkRTfRv1xNvwJ2qib4e3+RifQlo8VPQ==}
doctrine@2.1.0: doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2331,10 +2328,6 @@ packages:
resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==} resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==}
engines: {node: '>=16'} engines: {node: '>=16'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2771,6 +2764,11 @@ snapshots:
dependencies: dependencies:
'@types/react': 19.0.12 '@types/react': 19.0.12
'@types/react@17.0.2':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
'@types/react@18.3.1': '@types/react@18.3.1':
dependencies: dependencies:
'@types/prop-types': 15.7.14 '@types/prop-types': 15.7.14
@ -3257,6 +3255,11 @@ snapshots:
dependencies: dependencies:
path-type: 4.0.0 path-type: 4.0.0
discord-types@1.3.26:
dependencies:
'@types/react': 17.0.2
moment: 2.30.1
doctrine@2.1.0: doctrine@2.1.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@ -4920,8 +4923,6 @@ snapshots:
type-fest@4.38.0: {} type-fest@4.38.0: {}
type-fest@4.41.0: {}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4

View file

@ -31,7 +31,6 @@ const defines = stringifyValues({
IS_UPDATER_DISABLED, IS_UPDATER_DISABLED,
IS_WEB: false, IS_WEB: false,
IS_EXTENSION: false, IS_EXTENSION: false,
IS_USERSCRIPT: false,
VERSION, VERSION,
BUILD_TIMESTAMP BUILD_TIMESTAMP
}); });
@ -51,7 +50,7 @@ const nodeCommonOpts = {
format: "cjs", format: "cjs",
platform: "node", platform: "node",
target: ["esnext"], target: ["esnext"],
// @ts-expect-error this is never undefined // @ts-ignore this is never undefined
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external] external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external]
}; };

View file

@ -43,7 +43,6 @@ const commonOptions = {
define: stringifyValues({ define: stringifyValues({
IS_WEB: true, IS_WEB: true,
IS_EXTENSION: false, IS_EXTENSION: false,
IS_USERSCRIPT: false,
IS_STANDALONE: true, IS_STANDALONE: true,
IS_DEV, IS_DEV,
IS_REPORTER, IS_REPORTER,
@ -99,7 +98,6 @@ const buildConfigs = [
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])], inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: { define: {
...commonOptions.define, ...commonOptions.define,
IS_USERSCRIPT: "true",
window: "unsafeWindow", window: "unsafeWindow",
}, },
outfile: "dist/Vencord.user.js", outfile: "dist/Vencord.user.js",

View file

@ -363,6 +363,6 @@ export const commonRendererPlugins = [
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"), banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"), banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"), banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
// @ts-expect-error this is never undefined // @ts-ignore this is never undefined
...commonOpts.plugins ...commonOpts.plugins
]; ];

View file

@ -134,11 +134,7 @@ async function init() {
if (!IS_WEB && !IS_UPDATER_DISABLED) { if (!IS_WEB && !IS_UPDATER_DISABLED) {
runUpdateCheck(); runUpdateCheck();
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
// this tends to get really annoying, so only do this if the user has auto-update without notification enabled
if (Settings.autoUpdate && !Settings.autoUpdateNotification) {
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
}
} }
if (IS_DEV) { if (IS_DEV) {

View file

@ -5,7 +5,6 @@
*/ */
import type { Settings } from "@api/Settings"; import type { Settings } from "@api/Settings";
import { CspRequestResult } from "@main/csp/manager";
import { PluginIpcMappings } from "@main/ipcPlugins"; import { PluginIpcMappings } from "@main/ipcPlugins";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
@ -34,11 +33,10 @@ export default {
themes: { themes: {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData), uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName), deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST), getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName), getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES), getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
openFolder: () => invoke<void>(IpcEvents.OPEN_THEMES_FOLDER),
}, },
updater: { updater: {
@ -51,8 +49,7 @@ export default {
settings: { settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS), get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify), set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
openFolder: () => invoke<void>(IpcEvents.OPEN_SETTINGS_FOLDER),
}, },
quickCss: { quickCss: {
@ -76,17 +73,5 @@ export default {
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url) openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
}, },
csp: {
/**
* Note: Only supports full explicit matches, not wildcards.
*
* If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false.
*/
isDomainAllowed: (url: string, directives: string[]) => invoke<boolean>(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives),
removeOverride: (url: string) => invoke<boolean>(IpcEvents.CSP_REMOVE_OVERRIDE, url),
requestAddOverride: (url: string, directives: string[], callerName: string) =>
invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),
},
pluginHelpers: PluginHelpers pluginHelpers: PluginHelpers
}; };

View file

@ -8,9 +8,9 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Channel } from "@vencord/discord-types";
import { waitFor } from "@webpack"; import { waitFor } from "@webpack";
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>; let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;

View file

@ -17,11 +17,13 @@
*/ */
import { mergeDefaults } from "@utils/mergeDefaults"; import { mergeDefaults } from "@utils/mergeDefaults";
import { CommandArgument, Message } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest"; import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const createBotMessage = findByCodeLazy('username:"Clyde"'); const createBotMessage = findByCodeLazy('username:"Clyde"');
export function generateId() { export function generateId() {
@ -49,8 +51,8 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
* @param fallbackValue Fallback value in case this option wasn't passed * @param fallbackValue Fallback value in case this option wasn't passed
* @returns Value * @returns Value
*/ */
export function findOption<T>(args: CommandArgument[], name: string): T & {} | undefined; export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: CommandArgument[], name: string, fallbackValue: T): T & {}; export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: CommandArgument[], name: string, fallbackValue?: any) { export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value ?? fallbackValue) as any; return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
} }

View file

@ -18,39 +18,38 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import { CommandArgument, CommandContext, CommandOption } from "@vencord/discord-types";
import { sendBotMessage } from "./commandHelpers"; import { sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, VencordCommand } from "./types"; import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
export * from "./commandHelpers"; export * from "./commandHelpers";
export * from "./types"; export * from "./types";
export let BUILT_IN: VencordCommand[]; export let BUILT_IN: Command[];
export const commands = {} as Record<string, VencordCommand>; export const commands = {} as Record<string, Command>;
// hack for plugins being evaluated before we can grab these from webpack // hack for plugins being evaluated before we can grab these from webpack
const OptPlaceholder = Symbol("OptionalMessageOption") as any as CommandOption; const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as CommandOption; const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
/** /**
* Optional message option named "message" you can use in commands. * Optional message option named "message" you can use in commands.
* Used in "tableflip" or "shrug" * Used in "tableflip" or "shrug"
* @see {@link RequiredMessageOption} * @see {@link RequiredMessageOption}
*/ */
export let OptionalMessageOption: CommandOption = OptPlaceholder; export let OptionalMessageOption: Option = OptPlaceholder;
/** /**
* Required message option named "message" you can use in commands. * Required message option named "message" you can use in commands.
* Used in "me" * Used in "me"
* @see {@link OptionalMessageOption} * @see {@link OptionalMessageOption}
*/ */
export let RequiredMessageOption: CommandOption = ReqPlaceholder; export let RequiredMessageOption: Option = ReqPlaceholder;
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands // Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
// Add this offset to every added command to keep them unique // Add this offset to every added command to keep them unique
let commandIdOffset: number; let commandIdOffset: number;
export const _init = function (cmds: VencordCommand[]) { export const _init = function (cmds: Command[]) {
try { try {
BUILT_IN = cmds; BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0]; OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
@ -62,7 +61,7 @@ export const _init = function (cmds: VencordCommand[]) {
return cmds; return cmds;
} as never; } as never;
export const _handleCommand = function (cmd: VencordCommand, args: CommandArgument[], ctx: CommandContext) { export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
if (!cmd.isVencordCommand) if (!cmd.isVencordCommand)
return cmd.execute(args, ctx); return cmd.execute(args, ctx);
@ -93,7 +92,7 @@ export const _handleCommand = function (cmd: VencordCommand, args: CommandArgume
* Prepare a Command Option for Discord by filling missing fields * Prepare a Command Option for Discord by filling missing fields
* @param opt * @param opt
*/ */
export function prepareOption<O extends CommandOption | VencordCommand>(opt: O): O { export function prepareOption<O extends Option | Command>(opt: O): O {
opt.displayName ||= opt.name; opt.displayName ||= opt.name;
opt.displayDescription ||= opt.description; opt.displayDescription ||= opt.description;
opt.options?.forEach((opt, i, opts) => { opt.options?.forEach((opt, i, opts) => {
@ -110,7 +109,7 @@ export function prepareOption<O extends CommandOption | VencordCommand>(opt: O):
// Yes, Discord registers individual commands for each subcommand // Yes, Discord registers individual commands for each subcommand
// TODO: This probably doesn't support nested subcommands. If that is ever needed, // TODO: This probably doesn't support nested subcommands. If that is ever needed,
// investigate // investigate
function registerSubCommands(cmd: VencordCommand, plugin: string) { function registerSubCommands(cmd: Command, plugin: string) {
cmd.options?.forEach(o => { cmd.options?.forEach(o => {
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND) if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
throw new Error("When specifying sub-command options, all options must be sub-commands."); throw new Error("When specifying sub-command options, all options must be sub-commands.");
@ -133,7 +132,7 @@ function registerSubCommands(cmd: VencordCommand, plugin: string) {
}); });
} }
export function registerCommand<C extends VencordCommand>(command: C, plugin: string) { export function registerCommand<C extends Command>(command: C, plugin: string) {
if (!BUILT_IN) { if (!BUILT_IN) {
console.warn( console.warn(
"[CommandsAPI]", "[CommandsAPI]",

View file

@ -1,12 +1,106 @@
/* /*
* Vencord, a Discord client mod * Vencord, a modification for Discord's desktop app
* Copyright (c) 2025 Vendicated and contributors * Copyright (c) 2022 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later *
*/ * 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 <https://www.gnu.org/licenses/>.
*/
import { Command } from "@vencord/discord-types"; import { Channel, Guild } from "discord-types/general";
export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from "@vencord/discord-types/enums"; import { Promisable } from "type-fest";
export interface VencordCommand extends Command { export interface CommandContext {
isVencordCommand?: boolean; channel: Channel;
guild?: Guild;
}
export const enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export const enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export interface Option {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: Option[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export const enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export interface CommandReturnValue {
content: string;
/** TODO: implement */
cancel?: boolean;
}
export interface Argument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
options: Argument[];
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
isVencordCommand?: boolean;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];
predicate?(ctx: CommandContext): boolean;
execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
} }

View file

@ -22,9 +22,9 @@ export function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction, request: IDBRequest<T> | IDBTransaction,
): Promise<T> { ): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
// @ts-expect-error - file size hacks // @ts-ignore - file size hacks
request.oncomplete = request.onsuccess = () => resolve(request.result); request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-expect-error - file size hacks // @ts-ignore - file size hacks
request.onabort = request.onerror = () => reject(request.error); request.onabort = request.onerror = () => reject(request.error);
}); });
} }

View file

@ -17,7 +17,7 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, User } from "@vencord/discord-types"; import { Channel, User } from "discord-types/general/index.js";
import { JSX } from "react"; import { JSX } from "react";
interface DecoratorProps { interface DecoratorProps {

View file

@ -48,7 +48,7 @@ export function _modifyAccessories(
) { ) {
for (const [key, accessory] of accessories.entries()) { for (const [key, accessory] of accessories.entries()) {
const res = ( const res = (
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}> <ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
<accessory.render {...props} /> <accessory.render {...props} />
</ErrorBoundary> </ErrorBoundary>
); );

View file

@ -17,7 +17,7 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, Message } from "@vencord/discord-types"; import { Channel, Message } from "discord-types/general/index.js";
import { JSX } from "react"; import { JSX } from "react";
export interface MessageDecorationProps { export interface MessageDecorationProps {

View file

@ -17,8 +17,9 @@
*/ */
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import type { Channel, CustomEmoji, Message } from "@vencord/discord-types";
import { MessageStore } from "@webpack/common"; import { MessageStore } from "@webpack/common";
import { CustomEmoji } from "@webpack/types";
import type { Channel, Message } from "discord-types/general";
import type { Promisable } from "type-fest"; import type { Promisable } from "type-fest";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890"); const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");

View file

@ -18,7 +18,7 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Channel, Message } from "@vencord/discord-types"; import { Channel, Message } from "discord-types/general";
import type { ComponentType, MouseEventHandler } from "react"; import type { ComponentType, MouseEventHandler } from "react";
const logger = new Logger("MessagePopover"); const logger = new Logger("MessagePopover");

View file

@ -4,8 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { FluxStore, Message } from "@vencord/discord-types";
import { MessageCache, MessageStore } from "@webpack/common"; import { MessageCache, MessageStore } from "@webpack/common";
import { FluxStore } from "@webpack/types";
import { Message } from "discord-types/general";
/** /**
* Update and re-render a message * Update and re-render a message

View file

@ -3,8 +3,8 @@
all: unset; all: unset;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--text-default); color: var(--text-normal);
background-color: var(--background-base-lower-alt); background-color: var(--background-secondary-alt);
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
@ -12,7 +12,7 @@
} }
.visual-refresh .vc-notification-root { .visual-refresh .vc-notification-root {
background-color: var(--background-base-low); background-color: var(--bg-overlay-floating, var(--background-base-low));
} }
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) { .vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {

View file

@ -268,7 +268,7 @@ type ResolveUseSettings<T extends object> = {
[Key in keyof T]: [Key in keyof T]:
Key extends string Key extends string
? T[Key] extends Record<string, unknown> ? T[Key] extends Record<string, unknown>
// @ts-expect-error "Type instantiation is excessively deep and possibly infinite" // @ts-ignore "Type instantiation is excessively deep and possibly infinite"
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never ? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
: Key : Key
: never; : never;

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ButtonProps } from "@vencord/discord-types";
import { Button } from "@webpack/common"; import { Button } from "@webpack/common";
import { ButtonProps } from "@webpack/types";
import { Heart } from "./Heart"; import { Heart } from "./Heart";

View file

@ -75,15 +75,10 @@ const ErrorBoundary = LazyComponent(() => {
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
} }
get isNoop() {
if (IS_DEV) return false;
return this.props.noop;
}
render() { render() {
if (this.state.error === NO_ERROR) return this.props.children; if (this.state.error === NO_ERROR) return this.props.children;
if (this.isNoop) return null; if (this.props.noop) return null;
if (this.props.fallback) if (this.props.fallback)
return ( return (

View file

@ -3,9 +3,5 @@
background-color: #e7828430; background-color: #e7828430;
border: 1px solid #e78284; border: 1px solid #e78284;
border-radius: 5px; border-radius: 5px;
color: var(--text-default, white); color: var(--text-normal, white);
& a:hover {
text-decoration: underline;
}
} }

View file

@ -28,9 +28,6 @@ export function Link(props: React.PropsWithChildren<Props>) {
props.style.pointerEvents = "none"; props.style.pointerEvents = "none";
props["aria-disabled"] = true; props["aria-disabled"] = true;
} }
props.rel ??= "noreferrer";
return ( return (
<a role="link" target="_blank" {...props}> <a role="link" target="_blank" {...props}>
{props.children} {props.children}

View file

@ -14,8 +14,8 @@ import { DevsById } from "@utils/constants";
import { fetchUserProfile } from "@utils/discord"; import { fetchUserProfile } from "@utils/discord";
import { classes, pluralise } from "@utils/misc"; import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal"; import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { User } from "@vencord/discord-types";
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general";
import Plugins from "~plugins"; import Plugins from "~plugins";

View file

@ -26,12 +26,12 @@ import { Flex } from "@components/Flex";
import { gitRemote } from "@shared/vencordUserAgent"; import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin } from "@utils/types"; import { OptionType, Plugin } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest"; import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins"; import { PluginMeta } from "~plugins";
@ -212,7 +212,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const pluginMeta = PluginMeta[plugin.name]; const pluginMeta = PluginMeta[plugin.name];
return ( return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}> <ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
<ModalHeader separator={false}> <ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text> <Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
@ -268,9 +268,9 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div> </div>
</Forms.FormSection> </Forms.FormSection>
{!!plugin.settingsAboutComponent && ( {!!plugin.settingsAboutComponent && (
<div className={Margins.bottom8}> <div className={classes(Margins.bottom8, "vc-text-selectable")}>
<Forms.FormSection> <Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component"> <ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent tempSettings={tempSettings} /> <plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary> </ErrorBoundary>
</Forms.FormSection> </Forms.FormSection>

View file

@ -45,7 +45,7 @@ const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() =>
const cl = classNameFactory("vc-plugins-"); const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputWrapper", "inputError", "error"); const InputStyles = findByPropsLazy("inputWrapper", "inputDefault", "error");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled"); const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
@ -62,7 +62,7 @@ function showErrorToast(message: string) {
function ReloadRequiredCard({ required }: { required: boolean; }) { function ReloadRequiredCard({ required }: { required: boolean; }) {
return ( return (
<Card className={classes(cl("info-card"), required && "vc-warning-card")}> <Card className={cl("info-card", { "restart-card": required })}>
{required ? ( {required ? (
<> <>
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle> <Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
@ -349,7 +349,7 @@ export default function PluginSettings() {
select={onStatusChange} select={onStatusChange}
isSelected={v => v === searchValue.status} isSelected={v => v === searchValue.status}
closeOnSelect={true} closeOnSelect={true}
className={InputStyles.input} className={InputStyles.inputDefault}
/> />
</div> </div>
</div> </div>

View file

@ -66,6 +66,13 @@
gap: 0.25em; gap: 0.25em;
} }
.vc-plugins-restart-card {
padding: 1em;
background: var(--info-warning-background);
border: 1px solid var(--info-warning-foreground);
color: var(--info-warning-text);
}
.vc-plugins-restart-button { .vc-plugins-restart-button {
margin-top: 0.5em; margin-top: 0.5em;
background: var(--info-warning-foreground) !important; background: var(--info-warning-foreground) !important;

View file

@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid"; import { Grid } from "@components/Grid";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync"; import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common"; import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
@ -38,8 +38,6 @@ function validateUrl(url: string) {
} }
async function eraseAllData() { async function eraseAllData() {
if (!await checkCloudUrlCsp()) return;
const res = await fetch(new URL("/v1/", getCloudUrl()), { const res = await fetch(new URL("/v1/", getCloudUrl()), {
method: "DELETE", method: "DELETE",
headers: { Authorization: await getCloudAuth() } headers: { Authorization: await getCloudAuth() }

View file

@ -148,7 +148,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)} )}
{compileResult && {compileResult &&
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}> <Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
{compileResult[1]} {compileResult[1]}
</Forms.FormText> </Forms.FormText>
} }
@ -194,7 +194,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
error={error ?? replacementError} error={error ?? replacementError}
/> />
{!isFunc && ( {!isFunc && (
<div> <div className="vc-text-selectable">
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
{Object.entries({ {Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)", "\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",

View file

@ -18,21 +18,17 @@
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { openPluginModal } from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { showItemInFolder } from "@utils/native";
import { relaunch } from "@utils/native"; import { useAwaiter } from "@utils/react";
import { useForceUpdater } from "@utils/react";
import { getStylusWebStoreUrl } from "@utils/web";
import { findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -52,6 +48,62 @@ const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue &
const cl = classNameFactory("vc-settings-theme-"); const cl = classNameFactory("vc-settings-theme-");
function Validator({ link }: { link: string; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`;
const contentType = res.headers.get("Content-Type");
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
throw "Not a CSS file. Remember to use the raw link!";
return "Okay!";
}));
const text = pending
? "Checking..."
: err
? `Error: ${err instanceof Error ? err.message : String(err)}`
: "Valid!";
return <Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
}}>{text}</Forms.FormText>;
}
function Validators({ themeLinks }: { themeLinks: string[]; }) {
if (!themeLinks.length) return null;
return (
<>
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
}} key={link}>
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{label}
</Forms.FormTitle>
<Validator link={link} />
</Card>;
})}
</div>
</>
);
}
interface ThemeCardProps { interface ThemeCardProps {
theme: UserThemeHeader; theme: UserThemeHeader;
enabled: boolean; enabled: boolean;
@ -107,6 +159,7 @@ function ThemesTab() {
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n")); const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null); const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
useEffect(() => { useEffect(() => {
refreshLocalThemes(); refreshLocalThemes();
@ -166,12 +219,6 @@ function ThemesTab() {
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText> <Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
</Card> </Card>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
</Card>
<Forms.FormSection title="Local Themes"> <Forms.FormSection title="Local Themes">
<QuickActionCard> <QuickActionCard>
<> <>
@ -194,7 +241,8 @@ function ThemesTab() {
) : ( ) : (
<QuickAction <QuickAction
text="Open Themes Folder" text="Open Themes Folder"
action={() => VencordNative.themes.openFolder()} action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending}
Icon={FolderIcon} Icon={FolderIcon}
/> />
)} )}
@ -253,13 +301,7 @@ function ThemesTab() {
function renderOnlineThemes() { function renderOnlineThemes() {
return ( return (
<> <>
<Card className={classes("vc-warning-card", Margins.bottom16)}> <Card className="vc-settings-card vc-text-selectable">
<Forms.FormText>
This section is for advanced users. If you are having difficulties using it, use the
Local Themes tab instead.
</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText> <Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
@ -271,11 +313,12 @@ function ThemesTab() {
value={themeText} value={themeText}
onChange={setThemeText} onChange={setThemeText}
className={"vc-settings-theme-links"} className={"vc-settings-theme-links"}
placeholder="Enter Theme Links..." placeholder="Theme Links"
spellCheck={false} spellCheck={false}
onBlur={onBlur} onBlur={onBlur}
rows={10} rows={10}
/> />
<Validators themeLinks={settings.themeLinks} />
</Forms.FormSection> </Forms.FormSection>
</> </>
); );
@ -304,99 +347,10 @@ function ThemesTab() {
</TabBar.Item> </TabBar.Item>
</TabBar> </TabBar>
<CspErrorCard />
{currentTab === ThemeTab.LOCAL && renderLocalThemes()} {currentTab === ThemeTab.LOCAL && renderLocalThemes()}
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()} {currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
</SettingsTab> </SettingsTab>
); );
} }
export function CspErrorCard() { export default wrapTab(ThemesTab, "Themes");
if (IS_WEB) return null;
const errors = useCspErrors();
const forceUpdate = useForceUpdater();
if (!errors.length) return null;
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
const allowUrl = async (url: string) => {
const { origin: baseUrl, host } = new URL(url);
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
if (result !== "ok") return;
CspBlockedUrls.forEach(url => {
if (new URL(url).host === host) {
CspBlockedUrls.delete(url);
}
});
forceUpdate();
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
};
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
<ErrorCard className="vc-settings-card">
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
<Forms.FormText>
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
</Forms.FormText>
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
<div className="vc-settings-csp-list">
{errors.map((url, i) => (
<div key={url}>
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
<div className="vc-settings-csp-row">
<Link href={url}>{url}</Link>
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
Allow
</Button>
</div>
</div>
))}
</div>
{hasImgurHtmlDomain && (
<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
<Forms.FormText>
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
</Forms.FormText>
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
</>
)}
</ErrorCard>
);
}
function UserscriptThemesTab() {
return (
<SettingsTab title="Themes">
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
<Forms.FormText>
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
</Forms.FormText>
</Card>
</SettingsTab>
);
}
export default IS_USERSCRIPT
? wrapTab(UserscriptThemesTab, "Themes")
: wrapTab(ThemesTab, "Themes");

View file

@ -94,7 +94,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code> <code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{ <span style={{
marginLeft: "0.5em", marginLeft: "0.5em",
color: "var(--text-default)" color: "var(--text-normal)"
}}>{message} - {author}</span> }}>{message} - {author}</span>
</div> </div>
))} ))}
@ -225,7 +225,7 @@ function Updater() {
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle> <Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText> <Forms.FormText className="vc-text-selectable">
{repoPending {repoPending
? repo ? repo
: err : err

View file

@ -26,7 +26,8 @@ import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants"; import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity, isPluginDev } from "@utils/misc"; import { identity, isPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common"; import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
import BadgeAPI from "../../plugins/_api/badges"; import BadgeAPI from "../../plugins/_api/badges";
@ -52,6 +53,9 @@ type KeysOfType<Object, Type> = {
}[keyof Object]; }[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..."
});
const settings = useSettings(); const settings = useSettings();
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
@ -167,7 +171,7 @@ function VencordSettings() {
<QuickAction <QuickAction
Icon={FolderIcon} Icon={FolderIcon}
text="Open Settings Folder" text="Open Settings Folder"
action={() => VencordNative.settings.openFolder()} action={() => showItemInFolder(settingsDir)}
/> />
)} )}
<QuickAction <QuickAction

View file

@ -1,5 +1,5 @@
.vc-addon-card { .vc-addon-card {
background-color: var(--background-base-lower-alt); background-color: var(--background-secondary-alt);
color: var(--interactive-active); color: var(--interactive-active);
border-radius: 8px; border-radius: 8px;
display: block; display: block;

View file

@ -14,7 +14,7 @@
.vc-settings-quickActions-pill { .vc-settings-quickActions-pill {
all: unset; all: unset;
background: var(--background-base-lower); background: var(--background-secondary);
color: var(--header-secondary); color: var(--header-secondary);
display: flex; display: flex;
align-items: center; align-items: center;
@ -26,7 +26,7 @@
} }
.vc-settings-quickActions-pill:hover { .vc-settings-quickActions-pill:hover {
background: var(--background-base-lower-alt); background: var(--background-secondary-alt);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--elevation-high); box-shadow: var(--elevation-high);
} }

View file

@ -1,7 +1,7 @@
.vc-settings-tab-bar { .vc-settings-tab-bar {
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: 1px solid var(--border-subtle); border-bottom: 2px solid var(--background-modifier-accent);
} }
.vc-settings-tab-bar-item { .vc-settings-tab-bar-item {
@ -20,37 +20,29 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.vc-warning-card {
padding: 1em;
background: var(--info-warning-background);
border: 1px solid var(--info-warning-foreground);
color: var(--info-warning-foreground);
}
.vc-backup-restore-card { .vc-backup-restore-card {
background-color: var(--info-warning-background); background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground); border-color: var(--info-warning-foreground);
color: var(--info-warning-foreground); color: var(--info-warning-text);
} }
.vc-settings-theme-links { .vc-settings-theme-links {
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */ /* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
display: inline-block !important; display: inline-block !important;
color: var(--text-default) !important; color: var(--text-normal) !important;
padding: 0.5em 1em; padding: 0.5em;
border: 1px solid var(--input-border); border: 1px solid var(--background-modifier-accent);
max-height: unset; max-height: unset;
background-color: transparent; background-color: transparent;
box-sizing: border-box; box-sizing: border-box;
font-size: 12px;
line-height: 14px;
resize: none; resize: none;
width: 100%; width: 100%;
font-size: 1em;
line-height: 2em;
white-space: nowrap;
} }
.vc-settings-theme-links::placeholder { .vc-settings-theme-links::placeholder {
color: var(--text-muted) !important; color: var(--header-secondary);
} }
.vc-settings-theme-links:focus { .vc-settings-theme-links:focus {
@ -68,6 +60,15 @@
background-color: var(--button-danger-background); background-color: var(--button-danger-background);
} }
.vc-text-selectable,
.vc-text-selectable :where([class*="text" i], [class*="title" i]) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;
/* discord also sets cursor: default which prevents the cursor from showing as text */
cursor: initial;
}
.vc-updater-modal { .vc-updater-modal {
padding: 1.5em !important; padding: 1.5em !important;
} }

View file

@ -12,6 +12,11 @@
position: relative; position: relative;
} }
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
}
.vc-special-card-special { .vc-special-card-special {
padding: 1em 1.5em; padding: 1em 1.5em;
margin-bottom: 1em; margin-bottom: 1em;

View file

@ -7,7 +7,7 @@
.vc-settings-theme-card { .vc-settings-theme-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--background-base-lower-alt); background-color: var(--background-secondary-alt);
color: var(--interactive-active); color: var(--interactive-active);
border-radius: 8px; border-radius: 8px;
padding: 1em; padding: 1em;
@ -27,26 +27,3 @@
.vc-settings-theme-author::before { .vc-settings-theme-author::before {
content: "by "; content: "by ";
} }
.vc-settings-csp-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.vc-settings-csp-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
& a {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.2em;
}
--custom-button-button-md-height: 26px;
}

View file

@ -3,7 +3,7 @@
} }
.vc-owner-crown-icon { .vc-owner-crown-icon {
color: var(--status-warning); color: var(--text-warning);
} }
.vc-heart-icon { .vc-heart-icon {

View file

@ -6,10 +6,9 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import { ModuleFactory } from "@vencord/discord-types/webpack";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { wreq } from "@webpack"; import { wreq } from "@webpack";
import { AnyModuleFactory } from "webpack"; import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
export async function loadLazyChunks() { export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");

View file

@ -28,7 +28,7 @@ async function runReporter() {
} }
}, "Vencord Reporter"); }, "Vencord Reporter");
// @ts-expect-error // @ts-ignore
Vencord.Webpack._initReporter = function () { Vencord.Webpack._initReporter = function () {
// initReporter is called in the patched entry point of Discord // initReporter is called in the patched entry point of Discord
// setImmediate to only start searching for lazy chunks after Discord initialized the app // setImmediate to only start searching for lazy chunks after Discord initialized the app
@ -83,6 +83,7 @@ async function runReporter() {
result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports); result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail"); if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
} else { } else {
// @ts-ignore
result = Webpack[method](...args); result = Webpack[method](...args);
} }

3
src/globals.d.ts vendored
View file

@ -29,12 +29,11 @@ declare global {
* replace: "IS_WEB?foo:bar" * replace: "IS_WEB?foo:bar"
* // GOOD * // GOOD
* replace: IS_WEB ? "foo" : "bar" * replace: IS_WEB ? "foo" : "bar"
* // also okay * // also good
* replace: `${IS_WEB}?foo:bar` * replace: `${IS_WEB}?foo:bar`
*/ */
export var IS_WEB: boolean; export var IS_WEB: boolean;
export var IS_EXTENSION: boolean; export var IS_EXTENSION: boolean;
export var IS_USERSCRIPT: boolean;
export var IS_STANDALONE: boolean; export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean; export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean; export var IS_DEV: boolean;

View file

@ -1,157 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NativeSettings } from "@main/settings";
import { session } from "electron";
type PolicyMap = Record<string, string[]>;
export const ConnectSrc = ["connect-src"];
export const ImageSrc = [...ConnectSrc, "img-src"];
export const CssSrc = ["style-src", "font-src"];
export const ImageAndCssSrc = [...ImageSrc, ...CssSrc];
export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"];
// Plugins can whitelist their own domains by importing this object in their native.ts
// script and just adding to it. But generally, you should just edit this file instead
export const CspPolicies: PolicyMap = {
"http://localhost:*": ImageAndCssSrc,
"http://127.0.0.1:*": ImageAndCssSrc,
"localhost:*": ImageAndCssSrc,
"127.0.0.1:*": ImageAndCssSrc,
"*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes
"github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
"raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes
"*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes
"gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes
"*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes
"codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes
"*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes
"jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
"i.imgur.com": ImageSrc, // Imgur, used by some themes
"i.ibb.co": ImageSrc, // ImgBB, used by some themes
"i.pinimg.com": ImageSrc, // Pinterest, used by some themes
"*.tenor.com": ImageSrc, // Tenor, used by some themes
"files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes
"cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
"media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN
// CDNs used for some things by Vencord.
// FIXME: we really should not be using CDNs anymore
"cdnjs.cloudflare.com": ImageScriptsAndCssSrc,
"cdn.jsdelivr.net": ImageScriptsAndCssSrc,
// Function Specific
"api.github.com": ConnectSrc, // used for updating Vencord itself
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
"*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
"manti.vendicated.dev": ImageSrc, // ReviewDB API
"decor.fieryflames.dev": ConnectSrc, // Decor API
"ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN
"sponsor.ajay.app": ConnectSrc, // Dearrow API
"dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN
"usrbg.is-hardly.online": ImageSrc, // USRBG API
"icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search)
// forked addition
"*.dorkbutt.lol": ImageAndCssSrc,
};
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
const parsePolicy = (policy: string): PolicyMap => {
const result: PolicyMap = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyMap): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: PolicyMap) => {
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
if (reportOnlyHeader)
delete headers[reportOnlyHeader];
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
const pushDirective = (directive: string, ...values: string[]) => {
csp[directive] ??= [...(csp["default-src"] ?? [])];
csp[directive].push(...values);
};
pushDirective("style-src", "'unsafe-inline'");
// we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/
// HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline
// Once they stop using it, we also should
pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
pushDirective(directive, "blob:", "data:", "vencord:");
}
for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) {
for (const directive of directives) {
pushDirective(directive, host);
}
}
for (const [host, directives] of Object.entries(CspPolicies)) {
for (const directive of directives) {
pushDirective(directive, host);
}
}
headers[header] = [stringifyPolicy(csp)];
}
};
export function initCsp() {
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
}

View file

@ -1,125 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NativeSettings } from "@main/settings";
import { IpcEvents } from "@shared/IpcEvents";
import { dialog, ipcMain, IpcMainInvokeEvent } from "electron";
import { CspPolicies, ImageAndCssSrc } from ".";
export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict";
export function registerCspIpcHandlers() {
ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule);
ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule);
ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed);
}
function validate(url: string, directives: string[]) {
try {
const { host } = new URL(url);
if (/[;'"\\]/.test(host)) return false;
} catch {
return false;
}
if (directives.length === 0) return false;
if (directives.some(d => !ImageAndCssSrc.includes(d))) return false;
return true;
}
function getMessage(url: string, directives: string[], callerName: string) {
const domain = new URL(url).host;
const message = `${callerName} wants to allow connections to ${domain}`;
let detail =
`Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` +
`You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`;
if (directives.length === 1 && directives[0] === "connect-src") {
return { message, detail };
}
const contentTypes = directives
.filter(type => type !== "connect-src")
.map(type => {
switch (type) {
case "img-src":
return "Images";
case "style-src":
return "CSS & Themes";
case "font-src":
return "Fonts";
default:
throw new Error(`Illegal CSP directive: ${type}`);
}
})
.sort()
.join(", ");
detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`;
return { message, detail };
}
async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise<CspRequestResult> {
if (!validate(url, directives)) {
return "invalid";
}
const domain = new URL(url).host;
if (domain in NativeSettings.store.customCspRules) {
return "conflict";
}
const { checkboxChecked, response } = await dialog.showMessageBox({
...getMessage(url, directives, callerName),
type: callerName ? "info" : "warning",
title: "Vencord Host Permissions",
buttons: ["Cancel", "Allow"],
defaultId: 0,
cancelId: 0,
checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`,
checkboxChecked: false,
});
if (response !== 1) {
return "cancelled";
}
if (!checkboxChecked) {
return "unchecked";
}
NativeSettings.store.customCspRules[domain] = directives;
return "ok";
}
function removeCspRule(_: IpcMainInvokeEvent, domain: string) {
if (domain in NativeSettings.store.customCspRules) {
delete NativeSettings.store.customCspRules[domain];
return true;
}
return false;
}
function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) {
try {
const domain = new URL(url).host;
const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain];
if (!ruleForDomain) return false;
return directives.every(d => ruleForDomain.includes(d));
} catch (e) {
return false;
}
}

View file

@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { app, net, protocol } from "electron"; import { app, protocol, session } from "electron";
import { join } from "path"; import { join } from "path";
import { pathToFileURL } from "url";
import { initCsp } from "./csp";
import { ensureSafePath } from "./ipcMain"; import { ensureSafePath } from "./ipcMain";
import { RendererSettings } from "./settings"; import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
@ -28,27 +26,21 @@ import { installExt } from "./utils/extensions";
if (IS_VESKTOP || !IS_VANILLA) { if (IS_VESKTOP || !IS_VANILLA) {
app.whenReady().then(() => { app.whenReady().then(() => {
protocol.handle("vencord", ({ url: unsafeUrl }) => { // Source Maps! Maybe there's a better way but since the renderer is executed
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, ""); // from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1); if (url.endsWith("/")) url = url.slice(0, -1);
if (url.startsWith("/themes/")) { if (url.startsWith("/themes/")) {
const theme = url.slice("/themes/".length); const theme = url.slice("/themes/".length);
const safeUrl = ensureSafePath(THEMES_DIR, theme); const safeUrl = ensureSafePath(THEMES_DIR, theme);
if (!safeUrl) { if (!safeUrl) {
return new Response(null, { cb({ statusCode: 403 });
status: 404 return;
});
} }
cb(safeUrl.replace(/\?v=\d+$/, ""));
return net.fetch(pathToFileURL(safeUrl).toString()); return;
} }
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
switch (url) { switch (url) {
case "renderer.js.map": case "renderer.js.map":
case "vencordDesktopRenderer.js.map": case "vencordDesktopRenderer.js.map":
@ -56,11 +48,10 @@ if (IS_VESKTOP || !IS_VANILLA) {
case "vencordDesktopPreload.js.map": case "vencordDesktopPreload.js.map":
case "patcher.js.map": case "patcher.js.map":
case "vencordDesktopMain.js.map": case "vencordDesktopMain.js.map":
return net.fetch(pathToFileURL(join(__dirname, url)).toString()); cb(join(__dirname, url));
break;
default: default:
return new Response(null, { cb({ statusCode: 403 });
status: 404
});
} }
}); });
@ -72,7 +63,70 @@ if (IS_VESKTOP || !IS_VANILLA) {
} catch { } } catch { }
initCsp(); const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: Record<string, string[]>) => {
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] ??= [];
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
};
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
}); });
} }

View file

@ -28,17 +28,14 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises"; import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path"; import { join, normalize } from "path";
import { registerCspIpcHandlers } from "./csp/manager";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks"; import { makeLinksOpenExternally } from "./utils/externalLinks";
mkdirSync(THEMES_DIR, { recursive: true }); mkdirSync(THEMES_DIR, { recursive: true });
registerCspIpcHandlers();
export function ensureSafePath(basePath: string, path: string) { export function ensureSafePath(basePath: string, path: string) {
const normalizedBasePath = normalize(basePath + "/"); const normalizedBasePath = normalize(basePath);
const newPath = join(basePath, path); const newPath = join(basePath, path);
const normalizedPath = normalize(newPath); const normalizedPath = normalize(newPath);
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
@ -92,6 +89,7 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
writeFileSync(QUICKCSS_PATH, css) writeFileSync(QUICKCSS_PATH, css)
); );
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes()); ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName)); ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
@ -99,8 +97,6 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` "os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
})); }));
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
export function initIpc(mainWindow: BrowserWindow) { export function initIpc(mainWindow: BrowserWindow) {
let quickCssWatcher: FSWatcher | undefined; let quickCssWatcher: FSWatcher | undefined;

View file

@ -38,7 +38,7 @@ const asarPath = join(dirname(injectorPath), "..", asarName);
const discordPkg = require(join(asarPath, "package.json")); const discordPkg = require(join(asarPath, "package.json"));
require.main!.filename = join(asarPath, discordPkg.main); require.main!.filename = join(asarPath, discordPkg.main);
// @ts-expect-error Untyped method? Dies from cringe // @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath); app.setAppPath(asarPath);
if (!IS_VANILLA) { if (!IS_VANILLA) {

View file

@ -36,6 +36,7 @@ RendererSettings.addGlobalChangeListener(() => {
} }
}); });
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain); ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => { ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
@ -48,18 +49,16 @@ export interface NativeSettings {
[setting: string]: any; [setting: string]: any;
}; };
}; };
customCspRules: Record<string, string[]>;
} }
const DefaultNativeSettings: NativeSettings = { const DefaultNativeSettings: NativeSettings = {
plugins: {}, plugins: {}
customCspRules: {}
}; };
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE); const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
mergeDefaults(nativeSettings, DefaultNativeSettings); mergeDefaults(nativeSettings, DefaultNativeSettings);
export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings); export const NativeSettings = new SettingsStore(nativeSettings);
NativeSettings.addGlobalChangeListener(() => { NativeSettings.addGlobalChangeListener(() => {
try { try {

View file

@ -35,10 +35,7 @@ export function serializeErrors(func: (...args: any[]) => any) {
ok: false, ok: false,
error: e instanceof Error ? { error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object // prototypes get lost, so turn error into plain object
...e, ...e
message: e.message,
name: e.name,
stack: e.stack
} : e } : e
}; };
} }

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { fetchBuffer, fetchJson } from "@main/utils/http"; import { get } from "@main/utils/simpleGet";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`; const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, string][]; let PendingUpdates = [] as [string, string][];
async function githubGet<T = any>(endpoint: string) { async function githubGet(endpoint: string) {
return fetchJson<T>(API_BASE + endpoint, { return get(API_BASE + endpoint, {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header. // "All API requests MUST include a valid User-Agent header.
@ -46,8 +46,9 @@ async function calculateGitChanges() {
const isOutdated = await fetchUpdates(); const isOutdated = await fetchUpdates();
if (!isOutdated) return []; if (!isOutdated) return [];
const data = await githubGet(`/compare/${gitHash}...HEAD`); const res = await githubGet(`/compare/${gitHash}...HEAD`);
const data = JSON.parse(res.toString("utf-8"));
return data.commits.map((c: any) => ({ return data.commits.map((c: any) => ({
// github api only sends the long sha // github api only sends the long sha
hash: c.sha.slice(0, 7), hash: c.sha.slice(0, 7),
@ -57,8 +58,9 @@ async function calculateGitChanges() {
} }
async function fetchUpdates() { async function fetchUpdates() {
const data = await githubGet("/releases/latest"); const release = await githubGet("/releases/latest");
const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1); const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash) if (hash === gitHash)
return false; return false;
@ -68,20 +70,16 @@ async function fetchUpdates() {
PendingUpdates.push([name, browser_download_url]); PendingUpdates.push([name, browser_download_url]);
} }
}); });
return true; return true;
} }
async function applyUpdates() { async function applyUpdates() {
const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => { await Promise.all(PendingUpdates.map(
const contents = await fetchBuffer(url); async ([name, data]) => writeFile(
return [join(__dirname, name), contents] as const; join(__dirname, name),
})); await get(data)
)
await Promise.all(fileContents.map(async ([filename, contents]) => ));
writeFile(filename, contents))
);
PendingUpdates = []; PendingUpdates = [];
return true; return true;
} }

View file

@ -24,7 +24,7 @@ import { join } from "path";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { crxToZip } from "./crxToZip"; import { crxToZip } from "./crxToZip";
import { fetchBuffer } from "./http"; import { get } from "./simpleGet";
const extensionCacheDir = join(DATA_DIR, "ExtensionCache"); const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
@ -69,14 +69,13 @@ export async function installExt(id: string) {
} catch (err) { } catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`; const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
const buf = await fetchBuffer(url, { const buf = await get(url, {
headers: { headers: {
"User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)` "User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
} }
}); });
await extract(crxToZip(buf), extDir) await extract(crxToZip(buf), extDir).catch(console.error);
.catch(err => console.error(`Failed to extract extension ${id}`, err));
} }
session.defaultSession.loadExtension(extDir); session.defaultSession.loadExtension(extDir);

View file

@ -1,70 +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 <https://www.gnu.org/licenses/>.
*/
import { createWriteStream } from "original-fs";
import { Readable } from "stream";
import { finished } from "stream/promises";
type Url = string | URL;
export async function checkedFetch(url: Url, options?: RequestInit) {
try {
var res = await fetch(url, options);
} catch (err) {
if (err instanceof Error && err.cause) {
err = err.cause;
}
throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`);
}
if (res.ok) {
return res;
}
let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`;
try {
const reason = await res.text();
message += `\n${reason}`;
} catch { }
throw new Error(message);
}
export async function fetchJson<T = any>(url: Url, options?: RequestInit) {
const res = await checkedFetch(url, options);
return res.json() as Promise<T>;
}
export async function fetchBuffer(url: Url, options?: RequestInit) {
const res = await checkedFetch(url, options);
const buf = await res.arrayBuffer();
return Buffer.from(buf);
}
export async function downloadToFile(url: Url, path: string, options?: RequestInit) {
const res = await checkedFetch(url, options);
if (!res.body) {
throw new Error(`Download ${url}: response body is empty`);
}
// @ts-expect-error weird type conflict
const body = Readable.fromWeb(res.body);
await finished(body.pipe(createWriteStream(path)));
}

View file

@ -0,0 +1,37 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import https from "https";
export function get(url: string, options: https.RequestOptions = {}) {
return new Promise<Buffer>((resolve, reject) => {
https.get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400)
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300)
return void resolve(get(headers.location!, options));
const chunks = [] as Buffer[];
res.on("error", reject);
res.on("data", chunk => chunks.push(chunk));
res.once("end", () => resolve(Buffer.concat(chunks)));
});
});
}

View file

@ -30,10 +30,10 @@ import { Margins } from "@utils/margins";
import { shouldShowContributorBadge } from "@utils/misc"; import { shouldShowContributorBadge } from "@utils/misc";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { User } from "@vencord/discord-types";
import { Forms, Toasts, UserStore } from "@webpack/common"; import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
const ContributorBadge: ProfileBadge = { const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor", description: "Vencord Contributor",

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: { replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/, match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" + replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},` `]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
} }

View file

@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "@vencord/discord-types/webpack"; import { WebpackRequire } from "@webpack/wreq.d";
const settings = definePluginSettings({ const settings = definePluginSettings({
disableAnalytics: { disableAnalytics: {

View file

@ -213,7 +213,7 @@ export default definePlugin({
get chromiumVersion() { get chromiumVersion() {
try { try {
return VencordNative.native.getVersions().chrome return VencordNative.native.getVersions().chrome
// @ts-expect-error Typescript will add userAgentData IMMEDIATELY // @ts-ignore Typescript will add userAgentData IMMEDIATELY
|| navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version || navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version
|| null; || null;
} catch { // inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium } catch { // inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium

View file

@ -32,8 +32,8 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Channel } from "@vencord/discord-types";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { Channel } from "discord-types/general";
import { JSX } from "react"; import { JSX } from "react";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -196,6 +196,7 @@ export default definePlugin({
} }
} }
// @ts-ignore outdated type
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles; const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return; if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
@ -318,7 +319,7 @@ export default definePlugin({
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null; if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return ( return (
<Card className={`vc-warning-card ${Margins.top8}`}> <Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message Vencord plugin developers for support! Please do not private message Vencord plugin developers for support!
<br /> <br />
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")} Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}

View file

@ -9,9 +9,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common"; import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common";
import { User } from "discord-types/general";
interface UserProfileProps { interface UserProfileProps {
popoutProps: Record<string, any>; popoutProps: Record<string, any>;

Some files were not shown because too many files have changed in this diff Show more