Merge branch 'dev' into dev2

This commit is contained in:
thororen1234 2025-06-06 15:31:55 -04:00
commit ae0163d366
No known key found for this signature in database
72 changed files with 1264 additions and 620 deletions

View file

@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Extra included plugins
<details>
<summary>175 additional plugins</summary>
<summary>184 additional plugins</summary>
### All Platforms
@ -41,6 +41,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- CommandPalette by Ethan
- CopyStickerLinks by Byeoon
- CopyUserMention by Cortex & castdrian
- CustomFolderIcons by sadan
- CustomSounds by TheKodeToad & SpikeHD
- CustomTimestamps by Rini & nvhrr
- CustomUserColors by mochienya
@ -105,13 +106,17 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- Meow by Samwich
- MessageBurst by port
- MessageColors by Hen
- MessageFetchTimer by GroupXyz
- MessageLinkTooltip by Kyuuhachi
- MessageLoggerEnhanced by Aria
- MessageTranslate by Samwich
- ModalFade by Kyuuhachi
- MoreCommands by Arjix, echo, Samu
- MoreKaomoji by JacobTm & voidbbg
- MoreStickers by Leko & Arjix
- MoreUserTags by Cyn, TheSun, RyanCaoDev, LordElias, AutumnVN, hen
- Morse by zyqunix
- Moyai by Megu & Nuckyz
- NeverPausePreviews by vappstar
- NewPluginsManager by Sqaaakoi
- NoAppsAllowed by kvba
@ -123,6 +128,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- NoOnboarding by omaw & Glitch
- NoRoleHeaders by Samwich
- NotificationTitle by Kyuuhachi
- PartyMode by UwUDev
- PingNotifications by smuki
- PinIcon by iamme
- PlatformSpoofer by Drag
@ -154,6 +160,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- StatusPresets by iamme
- SteamStatusSync by niko
- StickerBlocker by Samwich
- StreamingCodecDisabler by davidkra230
- TalkInReverse by Tolgchu
- TeX by Kyuuhachi
- TextToSpeech by Samwich
@ -198,6 +205,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- ClipsEnhancements by niko
- MediaDownloader by Colorman
- NoRPC by Cyn
- StatusWhilePlaying by thororen
### Equicord Devbuilds Only

View file

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

View file

@ -236,7 +236,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
export function migratePluginSetting(pluginName: string, newSetting: string, oldSetting: string) {
const settings = SettingsStore.plain.plugins[pluginName];
if (!settings) return;

View file

@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
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() {
if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.noop) return null;
if (this.isNoop) return null;
if (this.props.fallback)
return (

View file

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

View file

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

View file

@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
{!!plugin.settingsAboutComponent && (
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary>
</Forms.FormSection>

View file

@ -244,7 +244,7 @@ export default function PluginSettings() {
}));
}, []);
const depMap = React.useMemo(() => {
const depMap = useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;

View file

@ -16,8 +16,8 @@ import { settings } from "../settings";
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
import { cl, getApplicationIcons } from "../utils";
// if discord one day decides to change their icon this needs to be updated
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
// Discord no longer shows an icon here by default but we use the one from the popout now here
const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
const icons: ActivityListIcon[] = [];
@ -86,7 +86,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
// We need to filter out custom statuses
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
if (shouldShow) {
return <DefaultActivityIcon />;
return <DefaultActivityIcon size="xs" />;
}
}

View file

@ -0,0 +1,124 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 sadan
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { closeModal, ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import { Button, Menu, Slider, TextInput, useState } from "@webpack/common";
import { folderIconsData, settings } from "./settings";
import { folderProp, int2rgba, setFolderData } from "./util";
export function ImageModal(folderProps: folderProp) {
const [data, setData] = useState(((settings.store.folderIcons ?? {}) as folderIconsData)[folderProps.folderId]?.url ?? "");
const [size, setSize] = useState(100);
return (
<>
<TextInput
// this looks like a horrorshow
defaultValue={data}
onChange={(val, _n) => {
setData(val);
}}
placeholder="https://example.com/image.png"
>
</TextInput>
<RenderPreview folderProps={folderProps} url={data} size={size} />
{data && <>
<div style={{
color: "#FFF"
}}>Change the size of the folder icon</div>
<Slider
initialValue={100}
onValueChange={(v: number) => {
setSize(v);
}}
maxValue={200}
minValue={25}
// [25, 200]
markers={Array.apply(0, Array(176)).map((_, i) => i + 25)}
stickToMarkers={true}
keyboardStep={1}
renderMarker={() => null} />
</>}
<Button onClick={() => {
setFolderData(folderProps, {
url: data,
size: size
});
closeModal("custom-folder-icon");
}}
>
Save
</Button>
<hr />
<Button onClick={() => {
// INFO: unset button
const folderSettings = settings.store.folderIcons as folderIconsData;
if (folderSettings[folderProps.folderId]) {
folderSettings[folderProps.folderId] = null;
}
closeModal("custom-folder-icon");
}}>
Unset
</Button>
<hr />
</>
);
}
export function RenderPreview({ folderProps, url, size }: { folderProps: folderProp; url: string; size: number; }) {
if (!url) return null;
return (
<div className="test1234" style={{
width: "20vh",
height: "20vh",
overflow: "hidden",
// 16/48
borderRadius: "33%",
backgroundColor: int2rgba(folderProps.folderColor, 0.4),
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<img alt="" src={url} width={`${size}%`} height={`${size}%`} />
</div>
);
}
export function makeContextItem(a: folderProp) {
return (
<Menu.MenuItem
id="custom-folder-icons"
key="custom-folder-icons"
label="Change Icon"
action={() => {
openModalLazy(async () => {
return props => (
<ModalRoot {...props}>
<ModalHeader >
<div style={{
color: "white"
}}>
Set a New Icon.
</div>
</ModalHeader>
<ModalContent>
<ImageModal folderId={a.folderId} folderColor={a.folderColor} />
</ModalContent>
<div style={{
color: "white",
margin: "2.5%",
marginTop: "1%"
}}>
You might have to hover the folder after setting in order for it to refresh.
</div>
</ModalRoot>
);
},
{
modalKey: "custom-folder-icon"
});
}} />
);
}

View file

@ -0,0 +1,58 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 sadan
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { makeContextItem } from "./components";
import { folderIconsData, settings } from "./settings";
import { folderProp, int2rgba } from "./util";
export default definePlugin({
name: "CustomFolderIcons",
description: "Customize folder icons with any png",
authors: [EquicordDevs.sadan],
settings,
patches: [
{
find: ".folderButtonInner",
replacement: {
match: /(\(0,\i\.jsx\)\(\i,\{folderNode:(\i),hovered:\i,sorting:\i\}\))/,
replace: "($self.shouldReplace({folderNode:$2})?$self.replace({folderNode:$2}):$1)"
}
},
],
contextMenus: {
"guild-context": (menuItems, props: folderProp) => {
if (!("folderId" in props)) return;
menuItems.push(makeContextItem(props));
}
},
shouldReplace(props: any): boolean {
return !!((settings.store.folderIcons as folderIconsData)?.[props.folderNode.id]?.url);
},
replace(props: any) {
const folderSettings = (settings.store.folderIcons as folderIconsData);
if (folderSettings && folderSettings[props.folderNode.id]) {
const data = folderSettings[props.folderNode.id];
return (
<div
style={{
backgroundColor: int2rgba(props.folderNode.color, +settings.store.solidIcon || .4),
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%"
}}
>
<img alt="" src={data!.url} width={`${data!.size ?? 100}%`} height={`${data!.size ?? 100}%`}
/>
</div>
);
}
}
});

View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 sadan
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
export interface folderIcon {
url: string,
size: number,
}
export type folderIconsData = Record<string, folderIcon | null>;
export const settings = definePluginSettings({
solidIcon: {
type: OptionType.BOOLEAN,
default: false,
description: "Use a solid background on the background of the image"
},
folderIcons: {
type: OptionType.COMPONENT,
hidden: true,
description: "folder icon settings",
component: () => <></>
}
});

View file

@ -0,0 +1,31 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 sadan
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { folderIcon, folderIconsData, settings } from "./settings";
export async function setFolderData(props: folderProp, newData: folderIcon) {
if (!settings.store.folderIcons) {
settings.store.folderIcons = {};
}
const folderSettings = (settings.store.folderIcons as folderIconsData);
folderSettings[props.folderId] = newData;
}
export interface folderProp {
folderId: string;
folderColor: number;
}
/**
* @param rgbVal RGB value
* @param alpha alpha bewteen zero and 1
*/
export function int2rgba(rgbVal: number, alpha: number = 1) {
const b = rgbVal & 0xFF,
g = (rgbVal & 0xFF00) >>> 8,
r = (rgbVal & 0xFF0000) >>> 16;
return `rgba(${[r, g, b].join(",")},${alpha})`;
}

View file

@ -147,7 +147,7 @@ function VencordPopoutButton() {
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
children.splice(
children.length - 1, 0,
<ErrorBoundary noop={true}>
<ErrorBoundary noop>
<VencordPopoutButton />
</ErrorBoundary>
);

View file

@ -9,9 +9,14 @@ import { GuildStore } from "@webpack/common";
import { RC } from "@webpack/types";
import { Channel, Guild, Message, User } from "discord-types/general";
import { settings } from "./settings";
import type { ITag } from "./types";
export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
export const isWebhook = (message: Message, user: User) => {
const isFollowed = message?.type === 0 && !!message?.messageReference && !settings.store.showWebhookTagFully;
return !!message?.webhookId && user.isNonUserBot() && !isFollowed;
};
export const tags = [
{
name: "WEBHOOK",

View file

@ -6,6 +6,7 @@
import "./styles.css";
import { migratePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs, EquicordDevs } from "@utils/constants";
import { getCurrentChannel, getIntlMessage } from "@utils/discord";
@ -31,11 +32,13 @@ const genTagTypes = () => {
return obj;
};
migratePluginSettings("ExpandedUserTags", "MoreUserTags");
export default definePlugin({
name: "MoreUserTags",
name: "ExpandedUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen],
dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"],
tags: ["MoreUserTags"],
settings,
patches: [
// Make discord actually use our tags
@ -73,7 +76,7 @@ export default definePlugin({
renderNicknameIcon(props) {
const tagId = this.getTag({
user: UserStore.getUser(props.userId),
channel: ChannelStore.getChannel(this.getChannelId()),
channel: getCurrentChannel(),
channelId: this.getChannelId(),
isChat: false
});
@ -89,7 +92,7 @@ export default definePlugin({
message: props.message,
user: UserStore.getUser(props.message.author.id),
channelId: props.message.channel_id,
isChat: false
isChat: true
});
return tagId && <Tag
@ -143,9 +146,9 @@ export default definePlugin({
const perms = this.getPermissions(user, channel);
for (const tag of tags) {
if (isChat && !settings.tagSettings[tag.name].showInChat)
if (isChat && !settings.tagSettings[tag.name]?.showInChat)
continue;
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
if (!isChat && !settings.tagSettings[tag.name]?.showInNotChat)
continue;
// If the owner tag is disabled, and the user is the owner of the guild,
@ -156,7 +159,9 @@ export default definePlugin({
user.id &&
isChat &&
!settings.tagSettings.OWNER.showInChat) ||
(!isChat &&
(GuildStore.getGuild(channel?.guild_id)?.ownerId ===
user.id &&
!isChat &&
!settings.tagSettings.OWNER.showInNotChat)
)
continue;

View file

@ -14,7 +14,7 @@ import { TagSettings } from "./types";
function SettingsComponent() {
const tagSettings = settings.store.tagSettings as TagSettings;
const { localTags } = Vencord.Plugins.plugins.MoreUserTags as any;
const { localTags } = Vencord.Plugins.plugins.ExpandedUserTags as any;
return (
<Flex flexDirection="column">
@ -96,9 +96,14 @@ export const settings = definePluginSettings({
default: false,
restartNeeded: true
},
showWebhookTagFully: {
description: "Show Webhook tag in followed channels like announcements",
type: OptionType.BOOLEAN,
default: false
},
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me"
}
},
});

View file

@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
return acc;
}, {} as Record<string, string>);
type ExtUpload = Upload & { fixExtension?: boolean; };
export default definePlugin({
name: "FixFileExtensions",
authors: [EquicordDevs.thororen],
@ -34,24 +32,21 @@ export default definePlugin({
patches: [
// Taken from AnonymiseFileNames
{
find: 'type:"UPLOAD_START"',
replacement: {
match: /await \i\.uploadFiles\((\i),/,
replace: "$1.forEach($self.fixExt),$&"
},
find: "async uploadFiles(",
replacement: [
{
match: /async uploadFiles\((\i),\i\){/,
replace: "$&$1.forEach($self.fixExt);"
},
{
match: /async uploadFilesSimple\((\i)\){/,
replace: "$&$1.forEach($self.fixExt);"
}
],
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
},
// Also taken from AnonymiseFileNames
{
find: 'addFilesTo:"message.attachments"',
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.fixExt(f)),$1",
},
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
}
],
fixExt(upload: ExtUpload) {
fixExt(upload: Upload) {
const file = upload.filename;
const tarMatch = tarExtMatcher.exec(file);
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");

View file

@ -24,7 +24,7 @@ export default async (
}: FurudoSettings,
repliedMessage?: Message
): Promise<string> => {
const completion = await fetch("http://localhost:11434/api/chat", {
const completion = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -30,7 +30,7 @@ export const settings = definePluginSettings({
},
showInMiniProfile: {
type: OptionType.BOOLEAN,
description: "Only show a button in the mini profile",
description: "Show full ui in the mini profile instead of just a button",
default: true
},
});

View file

@ -4,60 +4,31 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByProps } from "@webpack";
import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { VoiceState } from "@webpack/types";
const settings = definePluginSettings({
streamType: {
description: "Stream screen or window",
type: OptionType.SELECT,
options: [
{ label: "Screen", value: "screen" },
{ label: "Window", value: "window" }
],
default: "screen"
},
streamWindowKeyword: {
description: "Keyword to search for in window title",
type: OptionType.STRING,
default: "",
placeholder: "Enter keyword"
}
});
import { getCurrentMedia, settings } from "./utils";
const startStream = findByCodeLazy('type:"STREAM_START"');
let hasStreamed;
let sources;
let source;
async function startStream() {
const startStream = findByCode('type:"STREAM_START"');
const mediaEngine = findByProps("getMediaEngine").getMediaEngine();
const getDesktopSources = findByCode("desktop sources");
async function autoStartStream() {
const selected = SelectedChannelStore.getVoiceChannelId();
if (!selected) return;
const channel = ChannelStore.getChannel(selected);
if (channel.type === 13 || !PermissionStore.can(PermissionsBits.STREAM, channel)) return;
if (settings.store.streamType === "screen") {
sources = await getDesktopSources(mediaEngine, ["screen"], null);
source = sources[0];
} else if (settings.store.streamType === "window") {
const keyword = settings.store.streamWindowKeyword?.toLowerCase();
sources = await getDesktopSources(mediaEngine, ["window", "application"], null);
source = sources.find(s => s.name?.toLowerCase().includes(keyword));
}
if (!source) return;
const streamMedia = await getCurrentMedia();
startStream(channel.guild_id, selected, {
"pid": null,
"sourceId": source.id,
"sourceName": source.name,
"sourceId": streamMedia.id,
"sourceName": streamMedia.name,
"audioSourceId": null,
"sound": true,
"previewDisabled": false
@ -78,7 +49,7 @@ export default definePlugin({
if (channelId && !hasStreamed) {
hasStreamed = true;
await startStream();
await autoStartStream();
}
if (!channelId) {

View file

@ -0,0 +1,102 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Forms, SearchableSelect, useEffect, useState } from "@webpack/common";
interface PickerProps {
streamMediaSelection: any[];
streamMedia: any[];
}
const mediaEngine = findByPropsLazy("getMediaEngine");
const getDesktopSources = findByCodeLazy("desktop sources");
export const settings = definePluginSettings({
streamMedia: {
type: OptionType.COMPONENT,
component: SettingSection,
},
});
export async function getCurrentMedia() {
const media = mediaEngine.getMediaEngine();
const sources = [
...(await getDesktopSources(media, ["screen"], null) ?? []),
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
];
const streamMedia = sources.find(screen => screen.id === settings.store.streamMedia);
console.log(sources);
if (streamMedia) return streamMedia;
new Logger("InstantScreenShare").error(`Stream Media "${settings.store.streamMedia}" not found. Resetting to default.`);
settings.store.streamMedia = sources[0];
return sources[0];
}
function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps) {
const options = streamMediaSelection.map(screen => ({
label: screen.name,
value: screen.id,
default: streamMediaSelection[0],
}));
return (
<SearchableSelect
placeholder="Select a media source to stream "
maxVisibleItems={5}
options={options}
value={options.find(o => o.value === streamMedia)}
onChange={v => settings.store.streamMedia = v}
closeOnSelect
/>
);
}
function ScreenSetting() {
const { streamMedia } = settings.use(["streamMedia"]);
const media = mediaEngine.getMediaEngine();
const [streamMediaSelection, setStreamMediaSelection] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let active = true;
async function fetchMedia() {
setLoading(true);
const sources = [
...(await getDesktopSources(media, ["screen"], null) ?? []),
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
];
if (active) {
setStreamMediaSelection(sources);
setLoading(false);
}
}
fetchMedia();
return () => { active = false; };
}, []);
if (loading) return <Forms.FormText>Loading media sources...</Forms.FormText>;
if (!streamMediaSelection.length) return <Forms.FormText>No Media found.</Forms.FormText>;
return <StreamSimplePicker streamMediaSelection={streamMediaSelection} streamMedia={streamMedia} />;
}
function SettingSection() {
return (
<Forms.FormSection>
<Forms.FormTitle>Media source to stream</Forms.FormTitle>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Resets to main screen if not found</Forms.FormText>
<ScreenSetting />
</Forms.FormSection>
);
}

View file

@ -103,12 +103,21 @@ export async function stop(_: IpcMainInvokeEvent) {
}
async function metadata(options: DownloadOptions) {
stdout_global = "";
const metadata = JSON.parse(await ytdlp(["-J", options.url, "--no-warnings"]));
if (metadata.is_live) throw "Live streams are not supported.";
stdout_global = "";
return { videoTitle: `${metadata.title || "video"} (${metadata.id})` };
try {
stdout_global = "";
const output = await ytdlp(["-J", options.url, "--no-warnings"]);
const metadata = JSON.parse(output);
if (metadata.is_live) throw new Error("Live streams are not supported.");
stdout_global = "";
return { videoTitle: `${metadata.title || "video"} (${metadata.id})` };
} catch (err) {
throw err;
}
}
function genFormat({ videoTitle }: { videoTitle: string; }, { maxFileSize, format }: DownloadOptions) {
const HAS_LIMIT = !!maxFileSize;
const MAX_VIDEO_SIZE = HAS_LIMIT ? maxFileSize * 0.8 : 0;
@ -161,8 +170,11 @@ async function download({ format, videoTitle }: { format: string; videoTitle: st
: []
: [];
const customArgs = ytdlpArgs?.filter(Boolean) || [];
await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]);
try {
await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]);
} catch (err) {
console.error("Error during yt-dlp execution:", err);
}
const file = fs.readdirSync(getdir()).find(f => f.startsWith("download."));
if (!file) throw "No video file was found!";
return { file, videoTitle };

View file

@ -0,0 +1,170 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, React } from "@webpack/common";
interface FetchTiming {
channelId: string;
startTime: number;
endTime?: number;
duration?: number;
timestamp?: Date;
}
let currentFetch: FetchTiming | null = null;
let currentChannelId: string | null = null;
const channelTimings: Map<string, { time: number; timestamp: Date; }> = new Map();
const settings = definePluginSettings({
showIcon: {
type: OptionType.BOOLEAN,
description: "Show fetch time icon in message bar",
default: true,
},
showMs: {
type: OptionType.BOOLEAN,
description: "Show milliseconds in timing",
default: true,
},
iconColor: {
type: OptionType.STRING,
description: "Icon color (CSS color value)",
default: "#00d166",
}
});
const FetchTimeButton: ChatBarButtonFactory = ({ isMainChat }) => {
const { showMs, iconColor } = settings.use(["showMs", "iconColor"]);
if (!isMainChat || !settings.store.showIcon || !currentChannelId) {
return null;
}
const channelData = channelTimings.get(currentChannelId);
if (!channelData) {
return null;
}
const { time, timestamp } = channelData;
const displayTime = showMs ? `${Math.round(time)}ms` : `${Math.round(time / 1000)}s`;
if (!showMs && Math.round(time / 1000) === 0) {
return null;
}
const timeAgo = formatTimeAgo(timestamp);
return (
<ChatBarButton
tooltip={`Messages loaded in ${Math.round(time)}ms (${timeAgo})`}
onClick={() => { }}
>
<div style={{
display: "flex",
alignItems: "center",
gap: "4px"
}}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" />
</svg>
<span style={{
fontSize: "12px",
color: iconColor,
fontWeight: "500"
}}>
{displayTime}
</span>
</div>
</ChatBarButton>
);
};
function formatTimeAgo(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days} day${days > 1 ? "s" : ""} ago`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
} else {
return "just now";
}
}
function handleChannelSelect(data: any) {
if (data.channelId && data.channelId !== currentChannelId) {
currentChannelId = data.channelId;
currentFetch = {
channelId: data.channelId,
startTime: performance.now()
};
}
}
function handleMessageLoad(data: any) {
if (!currentFetch || data.channelId !== currentFetch.channelId) return;
const existing = channelTimings.get(currentFetch.channelId);
if (existing) return;
const endTime = performance.now();
const duration = endTime - currentFetch.startTime;
channelTimings.set(currentFetch.channelId, {
time: duration,
timestamp: new Date()
});
currentFetch = null;
}
export default definePlugin({
name: "MessageFetchTimer",
description: "Shows how long it took to fetch messages for the current channel",
authors: [EquicordDevs.GroupXyz],
settings,
start() {
FluxDispatcher.subscribe("CHANNEL_SELECT", handleChannelSelect);
FluxDispatcher.subscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad);
FluxDispatcher.subscribe("MESSAGE_CREATE", handleMessageLoad);
const currentChannel = getCurrentChannel();
if (currentChannel) {
currentChannelId = currentChannel.id;
}
},
stop() {
FluxDispatcher.unsubscribe("CHANNEL_SELECT", handleChannelSelect);
FluxDispatcher.unsubscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad);
FluxDispatcher.unsubscribe("MESSAGE_CREATE", handleMessageLoad);
currentFetch = null;
channelTimings.clear();
currentChannelId = null;
},
renderChatBarButton: FetchTimeButton,
});

View file

@ -78,7 +78,8 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo
bot: message?.bot || message?.author?.bot,
flags: message?.flags,
ghostPinged,
isCachedByUs: (message as LoggedMessageJSON).ourCache
isCachedByUs: (message as LoggedMessageJSON).ourCache,
webhookId: message?.webhookId
})
) {
// Flogger.log("IGNORING", message, payload);

View file

@ -52,6 +52,12 @@ export const settings = definePluginSettings({
}
},
ignoreWebhooks: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by webhooks",
default: false,
},
ignoreSelf: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by yourself",

View file

@ -74,6 +74,7 @@ interface ShouldIgnoreArguments {
bot?: boolean;
ghostPinged?: boolean;
isCachedByUs?: boolean;
webhookId?: string;
}
const EPHEMERAL = 64;
@ -87,7 +88,7 @@ const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
* @param {ShouldIgnoreArguments} args - An object containing the message details.
* @returns {boolean} - True if the message should be ignored, false if it should be kept.
*/
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs }: ShouldIgnoreArguments): boolean {
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs, webhookId }: ShouldIgnoreArguments): boolean {
const isEphemeral = ((flags ?? 0) & EPHEMERAL) === EPHEMERAL;
if (isEphemeral) return true; // ignore
@ -96,7 +97,7 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi
const myId = UserStore.getCurrentUser().id;
const { ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const { ignoreBots, ignoreSelf } = settings.store;
const { ignoreBots, ignoreSelf, ignoreWebhooks } = settings.store;
if (ignoreSelf && authorId === myId)
return true; // ignore
@ -132,6 +133,8 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi
if ((ignoreBots && bot) && !isAuthorWhitelisted) return true; // ignore
if ((ignoreWebhooks && webhookId) && !isAuthorWhitelisted) return true;
if (ghostPinged) return false; // keep
// author has highest priority

View file

@ -0,0 +1,65 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
function mock(input: string): string {
let output = "";
for (let i = 0; i < input.length; i++) {
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
}
return output;
}
export default definePlugin({
name: "MoreCommands",
description: "echo, lenny, mock",
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
commands: [
{
name: "echo",
description: "Sends a message as Clyde (locally)",
options: [OptionalMessageOption],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
const content = findOption(opts, "message", "");
sendBotMessage(ctx.channel.id, { content });
},
},
{
name: "lenny",
description: "Sends a lenny face",
options: [OptionalMessageOption],
execute: opts => ({
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
}),
},
{
name: "mock",
description: "mOcK PeOpLe",
options: [RequiredMessageOption],
execute: opts => ({
content: mock(findOption(opts, "message", ""))
}),
},
]
});

View file

@ -52,10 +52,8 @@ interface IVoiceChannelEffectSendEvent {
}
const MOYAI = "🗿";
const MOYAI_URL =
"https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3";
const MOYAI_URL_HD =
"https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav";
const MOYAI_URL = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3";
const MOYAI_URL_HD = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav";
const settings = definePluginSettings({
volume: {

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoRPC",
description: "Disables Discord's RPC server.",
authors: [Devs.Cyn],
patches: [
{
find: '.ensureModule("discord_rpc")',
replacement: {
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
replace: '.ensureModule("discord_rpc")}',
},
},
],
});

View file

@ -51,26 +51,27 @@ async function openCompleteQuestUI() {
const applicationId = quest.config.application.id;
const applicationName = quest.config.application.name;
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
// @ts-ignore
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
// @ts-ignore
const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
if (taskName === "WATCH_VIDEO") {
const tolerance = 2, speed = 10;
const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000);
const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded);
const maxFuture = 10, speed = 7, interval = 1;
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
const fn = async () => {
for (let i = startingPoint; i <= secondsNeeded; i += speed) {
try {
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } });
} catch (ex) {
console.log("Failed to send increment of", i, ex);
while (true) {
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
const diff = maxAllowed - secondsDone;
const timestamp = secondsDone + speed;
if (diff >= speed) {
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } });
secondsDone = Math.min(secondsNeeded, timestamp);
}
await new Promise(resolve => setTimeout(resolve, tolerance * 1000));
}
if ((secondsNeeded - secondsDone) % speed !== 0) {
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
if (timestamp >= secondsNeeded) {
break;
}
await new Promise(resolve => setTimeout(resolve, interval * 1000));
showNotification({
title: `${applicationName} - Quest Completer`,
body: "Quest Completed.",
@ -81,10 +82,9 @@ async function openCompleteQuestUI() {
fn();
showNotification({
title: `${applicationName} - Quest Completer`,
body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`,
body: `Spoofing video for ${applicationName}.`,
icon: icon,
});
console.log(`Spoofing video for ${applicationName}.`);
} else if (taskName === "PLAY_ON_DESKTOP") {
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
const appData = res.body[0];

View file

@ -61,8 +61,7 @@ const listener: MessageSendListener = async (channelId, msg) => {
if (hardSplit || splitIndex === -1) {
chunks.push(msg.content.slice(0, maxLength));
msg.content = msg.content.slice(maxLength);
}
else {
} else {
chunks.push(msg.content.slice(0, splitIndex));
msg.content = msg.content.slice(splitIndex);
}

View file

@ -0,0 +1,81 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
let mediaEngine = findStoreLazy("MediaEngineStore");
const originalCodecStatuses: {
AV1: boolean,
H265: boolean,
H264: boolean;
} = {
AV1: true,
H265: true,
H264: true
};
const settings = definePluginSettings({
disableAv1Codec: {
description: "Make Discord not consider using AV1 for streaming.",
type: OptionType.BOOLEAN,
default: false
},
disableH265Codec: {
description: "Make Discord not consider using H265 for streaming.",
type: OptionType.BOOLEAN,
default: false
},
disableH264Codec: {
description: "Make Discord not consider using H264 for streaming.",
type: OptionType.BOOLEAN,
default: false
},
});
export default definePlugin({
name: "StreamingCodecDisabler",
description: "Disable codecs for streaming of your choice",
authors: [EquicordDevs.davidkra230],
settings,
patches: [
{
find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo",
replacement: {
match: /setGoLiveSource\(.,.\)\{/,
replace: "$&$self.updateDisabledCodecs();"
},
}
],
async updateDisabledCodecs() {
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec);
mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec);
mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec);
},
async start() {
mediaEngine = mediaEngine.getMediaEngine();
const options = Object.keys(originalCodecStatuses);
// [{"codec":"","decode":false,"encode":false}]
const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res)));
CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => {
if (options.includes(codec.codec)) {
originalCodecStatuses[codec.codec] = codec.encode;
}
});
},
async stop() {
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1);
mediaEngine.setH265Enabled(originalCodecStatuses.H265);
mediaEngine.setH264Enabled(originalCodecStatuses.H264);
}
});

View file

@ -8,9 +8,10 @@ import * as DataStore from "@api/DataStore";
import { classNameFactory } from "@api/Styles";
import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Button, Forms, SearchableSelect, useMemo, useState } from "@webpack/common";
import { Button, Forms, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common";
import { DATASTORE_KEY, timezones } from ".";
import { DATASTORE_KEY, settings, timezones } from ".";
import { getTimezone, setTimezone, setUserDatabaseTimezone } from "./database";
export async function setUserTimezone(userId: string, timezone: string | null) {
timezones[userId] = timezone;
@ -19,9 +20,22 @@ export async function setUserTimezone(userId: string, timezone: string | null) {
const cl = classNameFactory("vc-timezone-");
export function SetTimezoneModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) {
export function SetTimezoneModal({ userId, modalProps, database }: { userId: string, modalProps: ModalProps; database?: boolean; }) {
const [currentValue, setCurrentValue] = useState<string | null>(timezones[userId] ?? null);
useEffect(() => {
const localTimezone = timezones[userId];
const shouldUseDatabase =
settings.store.useDatabase &&
(settings.store.preferDatabaseOverLocal || !localTimezone);
const value = shouldUseDatabase
? getTimezone(userId) ?? localTimezone
: localTimezone;
setCurrentValue(value ?? Intl.DateTimeFormat().resolvedOptions().timeZone);
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]);
const options = useMemo(() => {
return Intl.supportedValuesOf("timeZone").map(timezone => {
const offset = new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: "short" })
@ -59,20 +73,28 @@ export function SetTimezoneModal({ userId, modalProps }: { userId: string, modal
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Button
color={Button.Colors.RED}
onClick={async () => {
await setUserTimezone(userId, null);
modalProps.onClose();
}}
>
Delete Timezone
</Button>
{!database && (
<Button
color={Button.Colors.RED}
onClick={async () => {
await setUserTimezone(userId, null);
modalProps.onClose();
}}
>
Delete Timezone
</Button>
)}
<Button
color={Button.Colors.BRAND}
disabled={currentValue === null}
onClick={async () => {
await setUserTimezone(userId, currentValue!);
if (database) {
await setUserDatabaseTimezone(userId, currentValue);
await setTimezone(currentValue!);
} else {
await setUserTimezone(userId, currentValue);
}
modalProps.onClose();
}}
>

View file

@ -0,0 +1,110 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openModal } from "@utils/index";
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
const databaseTimezones: Record<string, { value: string | null; }> = {};
const DOMAIN = "https://timezone.creations.works";
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
const CLIENT_ID = "1377021506810417173";
export async function setUserDatabaseTimezone(userId: string, timezone: string | null) {
databaseTimezones[userId] = { value: timezone };
}
export function getTimezone(userId: string): string | null {
return databaseTimezones[userId]?.value ?? null;
}
export async function loadDatabaseTimezones(): Promise<boolean> {
try {
const res = await fetch(`${DOMAIN}/list`, {
headers: { Accept: "application/json" }
});
if (res.ok) {
const json = await res.json();
for (const id in json) {
databaseTimezones[id] = {
value: json[id]?.timezone ?? null
};
}
return true;
}
return false;
} catch (e) {
console.error("Failed to fetch timezones list:", e);
return false;
}
}
export async function setTimezone(timezone: string): Promise<boolean> {
const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
credentials: "include"
});
return res.ok;
}
export async function deleteTimezone(): Promise<boolean> {
const res = await fetch(`${DOMAIN}/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
credentials: "include"
});
return res.ok;
}
export function authModal(callback?: () => void) {
openModal(modalProps => (
<OAuth2AuthorizeModal
{...modalProps}
clientId={CLIENT_ID}
redirectUri={REDIRECT_URI}
responseType="code"
scopes={["identify"]}
permissions={0n}
cancelCompletesFlow={false}
callback={async (res: any) => {
if (!res || !res.location) return;
try {
const url = new URL(res.location);
const r = await fetch(url, {
credentials: "include",
headers: { Accept: "application/json" }
});
const json = await r.json();
if (!r.ok) {
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
return;
}
showToast("Authorization successful!", Toasts.Type.SUCCESS);
callback?.();
} catch (e) {
console.error("Error during authorization:", e);
showToast("Unexpected error during authorization", Toasts.Type.FAILURE);
}
}}
/>
));
}

View file

@ -1,6 +1,6 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@ -10,26 +10,29 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import { openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Menu, Tooltip, useEffect, useState } from "@webpack/common";
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
import { Message, User } from "discord-types/general";
import { authModal, deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
import { SetTimezoneModal } from "./TimezoneModal";
export const DATASTORE_KEY = "vencord-timezones";
export let timezones: Record<string, string | null> = {};
(async () => {
timezones = await DataStore.get<Record<string, string>>(DATASTORE_KEY) || {};
})();
export const DATASTORE_KEY = "vencord-timezones";
const classes = findByPropsLazy("timestamp", "compact", "contentOnly");
const locale = findByPropsLazy("getLocale");
export const settings = definePluginSettings({
"Show Own Timezone": {
type: OptionType.BOOLEAN,
description: "Show your own timezone in profiles and message headers",
default: true
},
"24h Time": {
type: OptionType.BOOLEAN,
description: "Show time in 24h format",
@ -46,6 +49,57 @@ export const settings = definePluginSettings({
type: OptionType.BOOLEAN,
description: "Show time in profiles",
default: true
},
useDatabase: {
type: OptionType.BOOLEAN,
description: "Enable database for getting user timezones",
default: true
},
preferDatabaseOverLocal: {
type: OptionType.BOOLEAN,
description: "Prefer database over local storage for timezones",
default: true
},
setDatabaseTimezone: {
description: "Set your timezone on the database",
type: OptionType.COMPONENT,
component: () => (
<Button onClick={() => {
authModal(async () => {
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
});
}}>
Set Timezone on Database
</Button>
)
},
resetDatabaseTimezone: {
description: "Reset your timezone on the database",
type: OptionType.COMPONENT,
component: () => (
<Button
color={Button.Colors.RED}
onClick={() => {
authModal(async () => {
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
await deleteTimezone();
});
}}
>
Reset Database Timezones
</Button>
)
},
askedTimezone: {
type: OptionType.BOOLEAN,
description: "Whether the user has been asked to set their timezone",
hidden: true,
default: false
}
});
@ -64,25 +118,36 @@ interface Props {
timestamp?: string;
type: "message" | "profile";
}
const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Props) => {
const [currentTime, setCurrentTime] = useState(timestamp || Date.now());
const timezone = timezones[userId];
const [timezone, setTimezone] = useState<string | null>(null);
useEffect(() => {
let timer: NodeJS.Timeout;
const localTimezone = timezones[userId];
const shouldUseDatabase =
settings.store.useDatabase &&
(settings.store.preferDatabaseOverLocal || !localTimezone);
if (type === "profile") {
setCurrentTime(Date.now());
const now = new Date();
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
timer = setTimeout(() => {
setCurrentTime(Date.now());
}, delay);
if (shouldUseDatabase) {
setTimezone(getTimezone(userId) ?? localTimezone);
} else {
setTimezone(localTimezone);
}
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]);
return () => timer && clearTimeout(timer);
useEffect(() => {
if (type !== "profile") return;
setCurrentTime(Date.now());
const now = new Date();
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
const timer = setTimeout(() => {
setCurrentTime(Date.now());
}, delay);
return () => clearTimeout(timer);
}, [type, currentTime]);
if (!timezone) return null;
@ -94,8 +159,9 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
minute: "numeric"
});
return (
<Tooltip
position="top"
@ -107,18 +173,14 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
tooltipClassName="timezone-tooltip"
text={longTime}
>
{toolTipProps => {
return (
<span
{...toolTipProps}
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
>
{
type === "message" ? `(${shortTime})` : shortTime
}
</span>
);
}}
{toolTipProps => (
<span
{...toolTipProps}
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
>
{type === "message" ? `(${shortTime})` : shortTime}
</span>
)}
</Tooltip>
);
}, { noop: true });
@ -128,19 +190,18 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: {
const setTimezoneItem = (
<Menu.MenuItem
label="Set Timezone"
label="Set Local Timezone"
id="set-timezone"
action={() => openModal(modalProps => <SetTimezoneModal userId={user.id} modalProps={modalProps} />)}
/>
);
children.push(<Menu.MenuSeparator />, setTimezoneItem);
};
export default definePlugin({
name: "Timezones",
authors: [Devs.Aria],
authors: [Devs.Aria, EquicordDevs.creations],
description: "Shows the local time of users in profiles and message headers",
contextMenus: {
"user-context": userContextMenuPatch
@ -164,26 +225,76 @@ export default definePlugin({
}
}
],
toolboxActions: {
"Set Database Timezone": () => {
authModal(async () => {
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
});
},
"Refresh Database Timezones": async () => {
try {
const good = await loadDatabaseTimezones();
if (good) {
showToast("Timezones refreshed successfully!", Toasts.Type.SUCCESS);
} else {
showToast("Timezones Failed to refresh!", Toasts.Type.FAILURE);
}
}
catch (error) {
console.error("Failed to refresh timezone:", error);
showToast("Failed to refresh timezones.", Toasts.Type.FAILURE);
}
}
},
async start() {
timezones = await DataStore.get<Record<string, string>>(DATASTORE_KEY) || {};
if (settings.store.useDatabase) {
await loadDatabaseTimezones();
if (!settings.store.askedTimezone) {
showToast(
"",
Toasts.Type.MESSAGE,
{
duration: 10000,
component: (
<Button
color={Button.Colors.GREEN}
onClick={() => {
authModal(async () => {
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
});
}}
>
Want to save your timezone to the database? Click here to set it.
</Button>
),
position: Toasts.Position.BOTTOM
}
);
settings.store.askedTimezone = true;
}
}
},
settings,
getTime,
renderProfileTimezone: (props?: { user?: User; }) => {
if (!settings.store.showProfileTime || !props?.user?.id) return null;
if (props.user.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null;
return <TimestampComponent
userId={props.user.id}
type="profile"
/>;
return <TimestampComponent userId={props.user.id} type="profile" />;
},
renderMessageTimezone: (props?: { message?: Message; }) => {
if (!settings.store.showMessageHeaderTime || !props?.message) return null;
if (props.message.author.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null;
return <TimestampComponent
userId={props.message.author.id}
timestamp={props.message.timestamp.toISOString()}
type="message"
/>;
return <TimestampComponent userId={props.message.author.id} timestamp={props.message.timestamp.toISOString()} type="message" />;
}
});

View file

@ -29,7 +29,7 @@ export default definePlugin({
authors: [Devs.AutumnVN],
start() {
(function connect() {
ws = new WebSocket("ws://localhost:24050/websocket/v2");
ws = new WebSocket("ws://127.0.0.1:24050/websocket/v2");
ws.addEventListener("error", () => ws.close());
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));

View file

@ -8,30 +8,30 @@ import { session } from "electron";
type PolicyMap = Record<string, string[]>;
const ConnectSrc = ["connect-src"];
const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
const CssSrc = ["style-src", "font-src"];
const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
export const ConnectSrc = ["connect-src"];
export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
export const CssSrc = ["style-src", "font-src"];
export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "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 = {
"*.github.io": MediaAndCssSrc, // github pages, used by most themes
"raw.githubusercontent.com": MediaAndCssSrc, // github raw, used by some themes
"*.gitlab.io": MediaAndCssSrc, // gitlab pages, used by some themes
"gitlab.com": MediaAndCssSrc, // gitlab raw, used by some themes
"*.codeberg.page": MediaAndCssSrc, // codeberg pages, used by some themes
"codeberg.org": MediaAndCssSrc, // codeberg raw, used by some themes
"*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes
"raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes
"*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes
"gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes
"*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes
"codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
"jsdelivr.net": MediaAndCssSrc, // jsdeliver, used by very few themes
"jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes
"fonts.googleapis.com": CssSrc, // google fonts, used by many themes
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
"i.imgur.com": MediaSrc, // imgur, used by some themes
"i.ibb.co": MediaSrc, // imgbb, used by some themes
"i.imgur.com": MediaSrc, // Imgur, used by some themes
"i.ibb.co": MediaSrc, // ImgBB, used by some themes
"cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
"media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
@ -43,7 +43,7 @@ export const CspPolicies: PolicyMap = {
// Function Specific
"api.github.com": ConnectSrc, // used for updating Vencord itself
"ws.audioscrobbler.com": ConnectSrc, // last.fm API
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
"*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
"manti.vendicated.dev": MediaSrc, // ReviewDB API
@ -78,7 +78,7 @@ const stringifyPolicy = (policy: PolicyMap): string =>
.join("; ");
const patchCsp = (headers: Record<string, string[]>) => {
const patchCsp = (headers: PolicyMap) => {
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
if (reportOnlyHeader)
delete headers[reportOnlyHeader];

View file

@ -25,7 +25,7 @@ import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
import { installExt } from "./utils/extensions";
if (IS_VESKTOP || IS_EQUIBOP || !IS_VANILLA) {
if (!IS_VANILLA && !IS_EXTENSION) {
app.whenReady().then(() => {
// 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

View file

@ -24,14 +24,14 @@ import { openContributorModal } from "@components/PluginSettings/ContributorModa
import { isEquicordDonor } from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { isEquicordPluginDev, isPluginDev } from "@utils/misc";
import { shouldShowContributorBadge, shouldShowEquicordContributorBadge } from "@utils/misc";
import definePlugin from "@utils/types";
import { Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
import { EquicordDonorModal, VencordDonorModal } from "./modals";
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
@ -39,7 +39,7 @@ const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor",
image: CONTRIBUTOR_BADGE,
position: BadgePosition.START,
shouldShow: ({ userId }) => isPluginDev(userId),
shouldShow: ({ userId }) => shouldShowContributorBadge(userId),
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
};
@ -47,7 +47,7 @@ const EquicordContributorBadge: ProfileBadge = {
description: "Equicord Contributor",
image: EQUICORD_CONTRIBUTOR_BADGE,
position: BadgePosition.START,
shouldShow: ({ userId }) => isEquicordPluginDev(userId),
shouldShow: ({ userId }) => shouldShowEquicordContributorBadge(userId),
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
};
@ -77,7 +77,7 @@ async function loadBadges(url: string, noCache = false) {
async function loadAllBadges(noCache = false) {
const vencordBadges = await loadBadges("https://badges.vencord.dev/badges.json", noCache);
const equicordBadges = await loadBadges("https://equicord.org/badges", noCache);
const equicordBadges = await loadBadges("https://equicord.org/badges.json", noCache);
DonorBadges = vencordBadges;
EquicordDonorBadges = equicordBadges;

View file

@ -30,8 +30,8 @@ export default definePlugin({
replacement: [
// Main setting definition
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
match: /\.updateAsync\(.+?(?=,useSetting:)/,
replace: "$&,userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1]"
},
// Selective wrapper
{

View file

@ -79,18 +79,17 @@ export default definePlugin({
patches: [
{
find: 'type:"UPLOAD_START"',
replacement: {
match: /await \i\.uploadFiles\((\i),/,
replace: "$1.forEach($self.anonymise),$&"
},
},
{
find: 'addFilesTo:"message.attachments"',
replacement: {
match: /\i.uploadFiles\((\i),/,
replace: "$1.forEach($self.anonymise),$&"
}
find: "async uploadFiles(",
replacement: [
{
match: /async uploadFiles\((\i),\i\){/,
replace: "$&$1.forEach($self.anonymise);"
},
{
match: /async uploadFilesSimple\((\i)\){/,
replace: "$&$1.forEach($self.anonymise);"
}
],
},
{
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",

View file

@ -36,12 +36,21 @@ async function lookupApp(applicationId: string): Promise<string> {
return socket.application;
}
let hideSetting = false;
if (IS_VESKTOP || IS_EQUIBOP || "legcord" in window) {
hideSetting = true;
} else if ("goofcord" in window) {
hideSetting = false;
}
let ws: WebSocket;
export default definePlugin({
name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
reporterTestable: ReporterTestable.None,
hidden: hideSetting,
settingsAboutComponent: () => (
<>
@ -73,9 +82,6 @@ export default definePlugin({
},
async start() {
// Legcord comes with its own arRPC implementation, so this plugin just confuses users
if ("legcord" in window) return;
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket

View file

@ -139,5 +139,5 @@ export default definePlugin({
}
},
DecorSection: ErrorBoundary.wrap(DecorSection)
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
});

View file

@ -17,6 +17,7 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
@ -165,7 +166,7 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
message = JSON.parse(e.text).message;
} catch { }
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
new Logger("ExpressionCloner").error("Failed to clone", data.name, "to", guildId, e);
Toasts.show({
message: "Failed to clone: " + message,
type: Toasts.Type.FAILURE,
@ -364,10 +365,11 @@ const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { t
}
};
migratePluginSettings("ExpressionCloner", "EmoteCloner");
export default definePlugin({
name: "EmoteCloner",
name: "ExpressionCloner",
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"],
tags: ["StickerCloner", "EmoteCloner", "EmojiCloner"],
authors: [Devs.Ven, Devs.Nuckyz],
contextMenus: {
"message": messageContextMenuPatch,

View file

@ -118,7 +118,7 @@ export default definePlugin({
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
this.instance = instance;
return (
<ErrorBoundary noop={true}>
<ErrorBoundary noop>
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
</ErrorBoundary>
);

View file

@ -98,7 +98,7 @@ export default definePlugin({
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/>
);
}),
}, { noop: true }),
});
function getUsernameString(username: string) {

View file

@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue";
@ -373,7 +372,7 @@ export default definePlugin({
settings,
start() {
addMessageAccessory("messageLinkEmbed", props => {
addMessageAccessory("MessageLinkEmbeds", props => {
if (!messageLinkRegex.test(props.message.content))
return null;
@ -381,15 +380,13 @@ export default definePlugin({
messageLinkRegex.lastIndex = 0;
return (
<ErrorBoundary>
<MessageEmbedAccessory
message={props.message}
/>
</ErrorBoundary>
<MessageEmbedAccessory
message={props.message}
/>
);
}, 4 /* just above rich embeds */);
},
stop() {
removeMessageAccessory("messageLinkEmbed");
removeMessageAccessory("MessageLinkEmbeds");
}
});

View file

@ -1,332 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
function mock(input: string): string {
let output = "";
for (let i = 0; i < input.length; i++) {
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
}
return output;
}
export default definePlugin({
name: "MoreCommands",
description: "Echo, Lenny, Mock, and More",
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
commands: [
{
name: "echo",
description: "Sends a message as Clyde (locally)",
options: [OptionalMessageOption],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
const content = findOption(opts, "message", "");
sendBotMessage(ctx.channel.id, { content });
},
},
{
name: "lenny",
description: "Sends a lenny face",
options: [OptionalMessageOption],
execute: opts => ({
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
}),
},
{
name: "mock",
description: "mOcK PeOpLe",
options: [RequiredMessageOption],
execute: opts => ({
content: mock(findOption(opts, "message", ""))
}),
},
{
name: "reverse",
description: "Reverses the input message",
options: [RequiredMessageOption],
execute: opts => ({
content: findOption(opts, "message", "").split("").reverse().join("")
}),
},
{
name: "uppercase",
description: "Converts the message to uppercase",
options: [RequiredMessageOption],
execute: opts => ({
content: findOption(opts, "message", "").toUpperCase()
}),
},
{
name: "lowercase",
description: "Converts the message to lowercase",
options: [RequiredMessageOption],
execute: opts => ({
content: findOption(opts, "message", "").toLowerCase()
}),
},
{
name: "wordcount",
description: "Counts the number of words in a message",
options: [RequiredMessageOption],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
const message = findOption(opts, "message", "");
const wordCount = message.trim().split(/\s+/).length;
sendBotMessage(ctx.channel.id, {
content: `The message contains ${wordCount} words.`
});
},
},
{
name: "ping",
description: "Pings the bot to check if it's responding",
options: [],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
sendBotMessage(ctx.channel.id, {
content: "Pong!"
});
},
},
{
name: "rolldice",
description: "Roll a die with the specified number of sides",
options: [RequiredMessageOption],
execute: opts => {
const sides = parseInt(findOption(opts, "message", "6"));
const roll = Math.floor(Math.random() * sides) + 1;
return {
content: `You rolled a ${roll}!`
};
},
},
{
name: "flipcoin",
description: "Flips a coin and returns heads or tails",
options: [],
execute: (opts, ctx) => {
const flip = Math.random() < 0.5 ? "Heads" : "Tails";
return {
content: `The coin landed on: ${flip}`
};
},
},
{
name: "ask",
description: "Ask a yes/no question and get an answer",
options: [RequiredMessageOption],
execute: opts => {
const question = findOption(opts, "message", "");
const responses = [
"Yes", "No", "Maybe", "Ask again later", "Definitely not", "It is certain"
];
const response = responses[Math.floor(Math.random() * responses.length)];
return {
content: `${question} - ${response}`
};
},
},
{
name: "randomcat",
description: "Get a random cat picture",
options: [],
execute: (opts, ctx) => {
return (async () => {
try {
const response = await fetch("https://api.thecatapi.com/v1/images/search");
if (!response.ok) throw new Error("Failed to fetch cat image");
const data = await response.json();
return {
content: data[0].url
};
} catch (err) {
sendBotMessage(ctx.channel.id, {
content: "Sorry, couldn't fetch a cat picture right now 😿"
});
}
})();
},
},
{
name: "randomdog",
description: "Get a random ddog picture",
options: [],
execute: (opts, ctx) => {
return (async () => {
try {
const response = await fetch("https://api.thedogapi.com/v1/images/search");
if (!response.ok) throw new Error("Failed to fetch dog image");
const data = await response.json();
return {
content: data[0].url
};
} catch (err) {
sendBotMessage(ctx.channel.id, {
content: "Sorry, couldn't fetch a cat picture right now 🐶"
});
}
})();
},
},
{
name: "randomnumber",
description: "Generates a random number between two values",
options: [
{
name: "min",
description: "Minimum value",
type: ApplicationCommandOptionType.INTEGER,
required: true
},
{
name: "max",
description: "Maximum value",
type: ApplicationCommandOptionType.INTEGER,
required: true
}
],
execute: opts => {
const min = parseInt(findOption(opts, "min", "0"));
const max = parseInt(findOption(opts, "max", "100"));
const number = Math.floor(Math.random() * (max - min + 1)) + min;
return {
content: `Random number between ${min} and ${max}: ${number}`
};
}
},
{
name: "countdown",
description: "Starts a countdown from a specified number",
options: [
{
name: "number",
description: "Number to countdown from (max 10)",
type: ApplicationCommandOptionType.INTEGER,
required: true
}
],
inputType: ApplicationCommandInputType.BOT,
execute: async (opts, ctx) => {
const number = Math.min(parseInt(findOption(opts, "number", "5")), 10);
if (isNaN(number) || number < 1) {
sendBotMessage(ctx.channel.id, {
content: "Please provide a valid number between 1 and 10!"
});
return;
}
sendBotMessage(ctx.channel.id, {
content: `Starting countdown from ${number}...`
});
for (let i = number; i >= 0; i--) {
await new Promise(resolve => setTimeout(resolve, 1000));
sendBotMessage(ctx.channel.id, {
content: i === 0 ? "🎉 Go! 🎉" : `${i}...`
});
}
},
},
{
name: "choose",
description: "Randomly chooses from provided options",
options: [
{
name: "choices",
description: "Comma-separated list of choices",
type: ApplicationCommandOptionType.STRING,
required: true
}
],
execute: opts => {
const choices = findOption(opts, "choices", "").split(",").map(c => c.trim());
const choice = choices[Math.floor(Math.random() * choices.length)];
return {
content: `I choose: ${choice}`
};
}
},
{
name: "systeminfo",
description: "Shows system information",
options: [],
execute: async (opts, ctx) => {
try {
const { userAgent, hardwareConcurrency, onLine, languages } = navigator;
const { width, height, colorDepth } = window.screen;
const { deviceMemory, connection }: { deviceMemory: any, connection: any; } = navigator as any;
const platform = userAgent.includes("Windows") ? "Windows" :
userAgent.includes("Mac") ? "MacOS" :
userAgent.includes("Linux") ? "Linux" : "Unknown";
const isMobile = /Mobile|Android|iPhone/i.test(userAgent);
const deviceType = isMobile ? "Mobile" : "Desktop";
const browserInfo = userAgent.match(/(?:chrome|firefox|safari|edge|opr)\/?\s*(\d+)/i)?.[0] || "Unknown";
const networkInfo = connection ? `${connection.effectiveType || "Unknown"}` : "Unknown";
const info = [
`> **Platform**: ${platform}`,
`> **Device Type**: ${deviceType}`,
`> **Browser**: ${browserInfo}`,
`> **CPU Cores**: ${hardwareConcurrency || "N/A"}`,
`> **Memory**: ${deviceMemory ? `${deviceMemory}GB` : "N/A"}`,
`> **Screen**: ${width}x${height} (${colorDepth}bit)`,
`> **Languages**: ${languages?.join(", ")}`,
`> **Network**: ${networkInfo} (${onLine ? "Online" : "Offline"})`
].join("\n");
return { content: info };
} catch (err) {
sendBotMessage(ctx.channel.id, { content: "Failed to fetch system information" });
}
},
},
{
name: "getUptime",
description: "Returns the system uptime",
execute: async (opts, ctx) => {
const uptime = performance.now() / 1000;
const uptimeInfo = `> **System Uptime**: ${Math.floor(uptime / 60)} minutes`;
return { content: uptimeInfo };
},
},
{
name: "getTime",
description: "Returns the current server time",
execute: async (opts, ctx) => {
const currentTime = new Date().toLocaleString();
return { content: `> **Current Time**: ${currentTime}` };
},
},
{
name: "getLocation",
description: "Returns the user's approximate location based on IP",
execute: async (opts, ctx) => {
try {
const response = await fetch("https://ipapi.co/json/");
const data = await response.json();
const locationInfo = `> **Country**: ${data.country_name}\n> **Region**: ${data.region}\n> **City**: ${data.city}`;
return { content: locationInfo };
} catch (err) {
sendBotMessage(ctx.channel.id, { content: "Failed to fetch location information" });
}
},
}
]
});

View file

@ -121,14 +121,9 @@ export default definePlugin({
},
// Make the gap between each item smaller so our tab can fit.
{
match: /className:\i\.tabBar/,
replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
match: /type:"top",/,
replace: '$&className:"vc-mutual-gdms-modal-v2-tab-bar",'
},
// Make the tab bar item text smaller so our tab can fit.
{
match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/,
replace: '$1"heading-sm/normal"'
}
]
},
{
@ -209,5 +204,5 @@ export default definePlugin({
/>
</>
);
})
}, { noop: true })
});

View file

@ -3,5 +3,5 @@
}
.vc-mutual-gdms-modal-v2-tab-bar {
gap: 12px;
--space-xl: 16px;
}

View file

@ -33,7 +33,7 @@ interface MessageDeleteProps {
}
// Remove this migration once enough time has passed
migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages");
migratePluginSetting("NoBlockedMessages", "ignoreMessages", "ignoreBlockedMessages");
const settings = definePluginSettings({
ignoreMessages: {
description: "Completely ignores incoming messages from blocked and ignored (if enabled) users",

View file

@ -1,35 +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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoRPC",
description: "Disables Discord's RPC server.",
authors: [Devs.Cyn],
patches: [
{
find: '.ensureModule("discord_rpc")',
replacement: {
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
replace: '.ensureModule("discord_rpc")}',
},
},
],
});

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "NoUnblockToJump",
description: "Allows you to jump to messages of blocked users without unblocking them",
description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them",
authors: [Devs.dzshn],
patches: [
{

View file

@ -75,5 +75,5 @@ export default definePlugin({
}}> Pause Indefinitely.</a>}
</div>
);
})
}, { noop: true })
});

View file

@ -201,7 +201,7 @@ function toggleMessageDecorators(enabled: boolean) {
}
}
migratePluginSetting("PlatformIndicators", "badges", "profiles");
migratePluginSetting("PlatformIndicators", "profiles", "badges");
const settings = definePluginSettings({
list: {
type: OptionType.BOOLEAN,

View file

@ -151,7 +151,13 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
: <button
{...tooltipProps}
className="vc-user-connection"
onClick={() => copyWithToast(connection.name)}
onClick={() => {
if (connection.type === "xbox") {
VencordNative.native.openExternal(`https://www.xbox.com/en-US/play/user/${encodeURIComponent(connection.name)}`);
} else {
copyWithToast(connection.name);
}
}}
>
{img}
</button>

View file

@ -299,7 +299,7 @@ export default definePlugin({
{
find: '"MessageManager"',
replacement: {
match: /"Skipping fetch because channelId is a static route"\);return}(?=.+?getChannel\((\i)\))/,
match: /forceFetch:\i,isPreload:.+?}=\i;(?=.+?getChannel\((\i)\))/,
replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;`
}
},
@ -526,10 +526,11 @@ export default definePlugin({
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
try {
if (!channel) return false;
if (channel == null || Object.hasOwn(channel, "channelId") && channel.channelId == null) return false;
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);
if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
if (["browse", "customize", "guide"].includes(channel.id)) return false;
return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);
} catch (e) {

View file

@ -86,5 +86,5 @@ export default definePlugin({
</TooltipContainer>
)}
</div>;
})
}, { noop: true })
});

View file

@ -132,10 +132,16 @@ export default definePlugin({
{
find: "Copy image not supported",
replacement: {
match: /(?<=(?:canSaveImage|canCopyImage)\(\i?\)\{.{0,50})!\i\.isPlatformEmbedded/g,
replace: "false"
}
replacement: [
{
match: /(?<=(?:canSaveImage|canCopyImage)\(.{0,120}?)!\i\.isPlatformEmbedded/g,
replace: "false"
},
{
match: /canCopyImage\(.+?(?=return"function"==typeof \i\.clipboard\.copyImage)/,
replace: "$&return true;"
}
]
},
// Add back Copy & Save Image
{
@ -147,7 +153,7 @@ export default definePlugin({
replace: "false"
},
{
match: /return\s*?\[.{0,50}?(?=\?.{0,100}?id:"copy-image")/,
match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/,
replace: "return [true"
},
{

View file

@ -67,7 +67,7 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`;
const cache = reactions[key] ??= { fetched: false, users: {} };
const cache = reactions[key] ??= { fetched: false, users: new Map() };
if (!cache.fetched) {
queue.unshift(() => fetchReactions(msg, e, type));
cache.fetched = true;
@ -169,7 +169,7 @@ export default definePlugin({
}, [message.id, forceUpdate]);
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Object.values(reactions).filter(Boolean) as User[];
const users = [...reactions.values()].filter(Boolean);
return (
<div
@ -201,7 +201,7 @@ export default definePlugin({
interface ReactionCacheEntry {
fetched: boolean;
users: Record<string, User>;
users: Map<string, User>;
}
interface RootObject {

View file

@ -57,7 +57,7 @@ export interface Dev {
*/
export const Devs = /* #__PURE__*/ Object.freeze({
Ven: {
name: "Vee",
name: "V",
id: 343383572805058560n
},
Arjix: {
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
},
axyie: {
name: "'ax",
id: 273562710745284628n
id: 929877747151548487n,
},
pointy: {
name: "pointy",
@ -604,7 +604,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
},
samsam: {
name: "samsam",
id: 836452332387565589n,
id: 400482410279469056n,
},
Cootshk: {
name: "Cootshk",
id: 921605971577548820n
},
} satisfies Record<string, Dev>);
@ -1074,6 +1078,18 @@ export const EquicordDevs = Object.freeze({
name: "sliwka",
id: 1165286199628419129n,
},
bbgaming25k: {
name: "bbgaming25k",
id: 851222385528274964n,
},
davidkra230: {
name: "davidkra230",
id: 652699312631054356n,
},
GroupXyz: {
name: "GroupXyz",
id: 950033410229944331n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useLayoutEffect } from "@webpack/common";
import { useForceUpdater } from "./react";
const cssRelevantDirectives = ["style-src", "img-src", "font-src"] as const;
export const CspBlockedUrls = new Set<string>();
const CspErrorListeners = new Set<() => void>();
document.addEventListener("securitypolicyviolation", ({ effectiveDirective, blockedURI }) => {
if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return;
CspBlockedUrls.add(blockedURI);
CspErrorListeners.forEach(listener => listener());
});
export function useCspErrors() {
const forceUpdate = useForceUpdater();
useLayoutEffect(() => {
CspErrorListeners.add(forceUpdate);
return () => void CspErrorListeners.delete(forceUpdate);
}, [forceUpdate]);
return [...CspBlockedUrls] as const;
}

View file

@ -21,6 +21,7 @@ export * from "../shared/onceDefined";
export * from "./ChangeList";
export * from "./clipboard";
export * from "./constants";
export * from "./cspViolations";
export * from "./discord";
export * from "./guards";
export * from "./intlHash";

View file

@ -92,7 +92,10 @@ export function identity<T>(value: T): T {
export const isMobile = navigator.userAgent.includes("Mobi");
export const isPluginDev = (id: string) => Object.hasOwn(VencordDevsById, id);
export const shouldShowContributorBadge = (id: string) => isPluginDev(id) && VencordDevsById[id].badge !== false;
export const isEquicordPluginDev = (id: string) => Object.hasOwn(EquicordDevsById, id);
export const shouldShowEquicordContributorBadge = (id: string) => isEquicordPluginDev(id) && EquicordDevsById[id].badge !== false;
export function pluralise(amount: number, singular: string, plural = singular + "s") {
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;

View file

@ -141,7 +141,7 @@ export const UserUtils = {
export const UploadManager = findByPropsLazy("clearAll", "addFile");
export const UploadHandler = {
promptToUpload: findByCodeLazy("#{intl::ATTACHMENT_TOO_MANY_ERROR_TITLE}") as (files: File[], channel: Channel, draftType: Number) => void
promptToUpload: findByCodeLazy("=!0,showLargeMessageDialog:") as (files: File[], channel: Channel, draftType: Number) => void
};
export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", {