Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
6eb61b0482
8 changed files with 51 additions and 695 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "equicord",
|
||||
"private": "true",
|
||||
"version": "1.15.6.0",
|
||||
"version": "1.15.7.0",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export function PatchPreview({ module, match, replacement, setReplacementError }
|
|||
|
||||
return (
|
||||
<>
|
||||
<Heading>Module {id}</Heading>
|
||||
<Heading style={{ userSelect: "text" }}>Module {id}</Heading>
|
||||
|
||||
<Match matchResult={matchResult} />
|
||||
<Diff diff={diff} />
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# AppleMusicRichPresence
|
||||
|
||||
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Devs, IS_MAC } from "@utils/constants";
|
||||
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
|
||||
import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types";
|
||||
import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "@vencord/discord-types/enums";
|
||||
import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common";
|
||||
|
||||
const Native = VencordNative.pluginHelpers.AppleMusicRichPresence as PluginNative<typeof import("./native")>;
|
||||
|
||||
export interface TrackData {
|
||||
name: string;
|
||||
album?: string;
|
||||
artist?: string;
|
||||
|
||||
appleMusicLink?: string;
|
||||
appleMusicArtistLink?: string;
|
||||
songLink?: string;
|
||||
|
||||
albumArtwork?: string;
|
||||
artistArtwork?: string;
|
||||
|
||||
playerPosition?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const enum AssetImageType {
|
||||
Album = "Album",
|
||||
Artist = "Artist",
|
||||
Disabled = "Disabled"
|
||||
}
|
||||
|
||||
const enum LinkType {
|
||||
Album = "Album",
|
||||
Artist = "Artist",
|
||||
Disabled = "Disabled"
|
||||
}
|
||||
|
||||
const applicationId = "1239490006054207550";
|
||||
|
||||
let updateInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
function setActivity(activity: Activity | null) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity,
|
||||
socketId: "AppleMusic",
|
||||
});
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
activityType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Which type of activity",
|
||||
options: [
|
||||
{ label: "Playing", value: ActivityType.PLAYING, default: true },
|
||||
{ label: "Listening", value: ActivityType.LISTENING }
|
||||
],
|
||||
},
|
||||
statusDisplayType: {
|
||||
description: "Show the track / artist name in the member list",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Don't show (shows generic listening message)",
|
||||
value: "off",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Show artist name",
|
||||
value: "artist"
|
||||
},
|
||||
{
|
||||
label: "Show track name",
|
||||
value: "track"
|
||||
}
|
||||
]
|
||||
},
|
||||
refreshInterval: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "The interval between activity refreshes (seconds)",
|
||||
markers: [1, 2, 2.5, 3, 5, 10, 15],
|
||||
default: 5,
|
||||
restartNeeded: true,
|
||||
},
|
||||
enableTimestamps: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether or not to enable timestamps",
|
||||
default: true,
|
||||
},
|
||||
enableButtons: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether or not to enable buttons",
|
||||
default: true,
|
||||
},
|
||||
nameString: {
|
||||
type: OptionType.STRING,
|
||||
description: "Activity name format string",
|
||||
default: "Apple Music"
|
||||
},
|
||||
detailsString: {
|
||||
type: OptionType.STRING,
|
||||
description: "Activity details format string",
|
||||
default: "{name}"
|
||||
},
|
||||
stateString: {
|
||||
type: OptionType.STRING,
|
||||
description: "Activity state format string",
|
||||
default: "{artist} · {album}"
|
||||
},
|
||||
detailsLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity details link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album, default: true },
|
||||
{ label: "Artist", value: LinkType.Artist },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
stateLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity state link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album },
|
||||
{ label: "Artist", value: LinkType.Artist, default: true },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
largeImageType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets large image type",
|
||||
options: [
|
||||
{ label: "Album artwork", value: AssetImageType.Album, default: true },
|
||||
{ label: "Artist artwork", value: AssetImageType.Artist },
|
||||
{ label: "Disabled", value: AssetImageType.Disabled }
|
||||
],
|
||||
},
|
||||
largeTextString: {
|
||||
type: OptionType.STRING,
|
||||
description: "Activity assets large text format string",
|
||||
default: "{album}"
|
||||
},
|
||||
largeImageLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets large image link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album, default: true },
|
||||
{ label: "Artist", value: LinkType.Artist },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
smallImageType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets small image type",
|
||||
options: [
|
||||
{ label: "Album artwork", value: AssetImageType.Album },
|
||||
{ label: "Artist artwork", value: AssetImageType.Artist, default: true },
|
||||
{ label: "Disabled", value: AssetImageType.Disabled }
|
||||
],
|
||||
},
|
||||
smallTextString: {
|
||||
type: OptionType.STRING,
|
||||
description: "Activity assets small text format string",
|
||||
default: "{artist}"
|
||||
},
|
||||
smallImageLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets small image link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album },
|
||||
{ label: "Artist", value: LinkType.Artist, default: true },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function customFormat(formatStr: string, data: TrackData) {
|
||||
return formatStr
|
||||
.replaceAll("{name}", data.name)
|
||||
.replaceAll("{album}", data.album ?? "")
|
||||
.replaceAll("{artist}", data.artist ?? "");
|
||||
}
|
||||
|
||||
function getLink(type: LinkType, data: TrackData) {
|
||||
return type === LinkType.Album
|
||||
? data.appleMusicLink
|
||||
: type === LinkType.Artist
|
||||
? data.appleMusicArtistLink
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getImageAsset(type: AssetImageType, data: TrackData) {
|
||||
const source = type === AssetImageType.Album
|
||||
? data.albumArtwork
|
||||
: data.artistArtwork;
|
||||
|
||||
if (!source) return undefined;
|
||||
|
||||
return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "AppleMusicRichPresence",
|
||||
description: "Discord rich presence for your Apple Music!",
|
||||
tags: ["Activity", "Media"],
|
||||
authors: [Devs.RyanCaoDev],
|
||||
hidden: !IS_MAC,
|
||||
reporterTestable: ReporterTestable.None,
|
||||
|
||||
settingsAboutComponent() {
|
||||
return <>
|
||||
<Paragraph>
|
||||
For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
|
||||
<code>{"{name}"}</code> is replaced with the track name; <code>{"{artist}"}</code> is replaced with the artist(s)' name(s); and <code>{"{album}"}</code> is replaced with the album name.
|
||||
</Paragraph>
|
||||
</>;
|
||||
},
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
this.updatePresence();
|
||||
updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(updateInterval);
|
||||
updateInterval = undefined;
|
||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
|
||||
},
|
||||
|
||||
updatePresence() {
|
||||
this.getActivity().then(activity => { setActivity(activity); });
|
||||
},
|
||||
|
||||
async getActivity(): Promise<Activity | null> {
|
||||
const trackData = await Native.fetchTrackData();
|
||||
if (!trackData) return null;
|
||||
|
||||
const [largeImageAsset, smallImageAsset] = await Promise.all([
|
||||
getImageAsset(settings.store.largeImageType, trackData),
|
||||
getImageAsset(settings.store.smallImageType, trackData)
|
||||
]);
|
||||
|
||||
const assets: ActivityAssets = {};
|
||||
|
||||
const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);
|
||||
|
||||
if (settings.store.largeImageType !== AssetImageType.Disabled) {
|
||||
assets.large_image = largeImageAsset;
|
||||
if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
|
||||
assets.large_url = getLink(settings.store.largeImageLink, trackData);
|
||||
}
|
||||
|
||||
if (settings.store.smallImageType !== AssetImageType.Disabled) {
|
||||
assets.small_image = smallImageAsset;
|
||||
if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
|
||||
assets.small_url = getLink(settings.store.smallImageLink, trackData);
|
||||
}
|
||||
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
||||
if (settings.store.enableButtons) {
|
||||
if (trackData.appleMusicLink)
|
||||
buttons.push({
|
||||
label: "Listen on Apple Music",
|
||||
url: trackData.appleMusicLink,
|
||||
});
|
||||
|
||||
if (trackData.songLink)
|
||||
buttons.push({
|
||||
label: "View on SongLink",
|
||||
url: trackData.songLink,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
application_id: applicationId,
|
||||
|
||||
name: customFormat(settings.store.nameString, trackData),
|
||||
details: customFormat(settings.store.detailsString, trackData),
|
||||
state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
|
||||
details_url: getLink(settings.store.detailsLink, trackData),
|
||||
state_url: getLink(settings.store.stateLink, trackData),
|
||||
|
||||
timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
|
||||
start: Date.now() - (trackData.playerPosition * 1000),
|
||||
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
|
||||
} : undefined,
|
||||
|
||||
assets,
|
||||
|
||||
buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,
|
||||
|
||||
type: settings.store.activityType,
|
||||
status_display_type: {
|
||||
"off": ActivityStatusDisplayType.NAME,
|
||||
"artist": ActivityStatusDisplayType.STATE,
|
||||
"track": ActivityStatusDisplayType.DETAILS
|
||||
}[settings.store.statusDisplayType],
|
||||
flags: ActivityFlags.INSTANCE,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
import type { TrackData } from ".";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
async function applescript(cmds: string[]) {
|
||||
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
|
||||
return stdout;
|
||||
}
|
||||
|
||||
interface RemoteData {
|
||||
appleMusicLink?: string,
|
||||
appleMusicArtistLink?: string;
|
||||
songLink?: string,
|
||||
albumArtwork?: string,
|
||||
artistArtwork?: string;
|
||||
}
|
||||
|
||||
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
||||
|
||||
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
|
||||
if (id === cachedRemoteData?.id) {
|
||||
if ("data" in cachedRemoteData) return cachedRemoteData.data;
|
||||
if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = new URL("https://itunes.apple.com/search");
|
||||
dataUrl.searchParams.set("term", `${name} ${artist} ${album}`);
|
||||
dataUrl.searchParams.set("media", "music");
|
||||
dataUrl.searchParams.set("entity", "song");
|
||||
|
||||
const fetchData = () => fetch(dataUrl, {
|
||||
headers: {
|
||||
"user-agent": VENCORD_USER_AGENT,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
|
||||
let data = await fetchData();
|
||||
|
||||
if (data.resultCount === 0) {
|
||||
dataUrl.searchParams.set("term", `${name} ${artist}`);
|
||||
data = await fetchData();
|
||||
}
|
||||
|
||||
const songData = data.results.find(song => song.collectionName === album) || data.results[0];
|
||||
|
||||
const artistArtworkURL = await fetch(songData.artistViewUrl)
|
||||
.then(r => r.text())
|
||||
.then(data => {
|
||||
const match = data.match(/<meta property="og:image" content="(.+?)">/);
|
||||
return match ? match[1].replace(/[0-9]+x.+/, "220x220bb-60.png") : undefined;
|
||||
})
|
||||
.catch(() => void 0);
|
||||
|
||||
cachedRemoteData = {
|
||||
id,
|
||||
data: {
|
||||
appleMusicLink: songData.trackViewUrl,
|
||||
appleMusicArtistLink: songData.artistViewUrl,
|
||||
songLink: `https://song.link/i/${new URL(songData.trackViewUrl).searchParams.get("i")}`,
|
||||
albumArtwork: (songData.artworkUrl100).replace("100x100", "512x512"),
|
||||
artistArtwork: artistArtworkURL
|
||||
}
|
||||
};
|
||||
|
||||
return cachedRemoteData.data;
|
||||
} catch (e) {
|
||||
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
|
||||
cachedRemoteData = {
|
||||
id,
|
||||
failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
|
||||
};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTrackData(): Promise<TrackData | null> {
|
||||
try {
|
||||
await exec("pgrep", ["^Music$"]);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
|
||||
.then(out => out.trim());
|
||||
if (playerState !== "playing") return null;
|
||||
|
||||
const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
|
||||
.then(text => Number.parseFloat(text.trim()));
|
||||
|
||||
const stdout = await applescript([
|
||||
'set output to ""',
|
||||
'tell application "Music"',
|
||||
"set t_id to database id of current track",
|
||||
"set t_name to name of current track",
|
||||
"set t_album to album of current track",
|
||||
"set t_artist to artist of current track",
|
||||
"set t_duration to duration of current track",
|
||||
'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
|
||||
"end tell",
|
||||
"return output"
|
||||
]);
|
||||
|
||||
const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
|
||||
const duration = Number.parseFloat(durationStr);
|
||||
|
||||
const remoteData = await fetchRemoteData({ id, name, artist, album });
|
||||
|
||||
return { name, album, artist, playerPosition, duration, ...remoteData };
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ import definePlugin, { OptionType } from "@utils/types";
|
|||
import type { Emoji, Message, RenderModalProps, Sticker } from "@vencord/discord-types";
|
||||
import { StickerFormatType } from "@vencord/discord-types/enums";
|
||||
import { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from "@webpack";
|
||||
import { ChannelStore, ConfirmModal, DraftType, EmojiStore, FluxDispatcher, GuildMemberStore, IconUtils, lodash, openModal, OverridePremiumTypeStore, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore } from "@webpack/common";
|
||||
import { ChannelStore, ConfirmModal, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, openModal, OverridePremiumTypeStore, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
|
|
@ -41,6 +41,8 @@ function searchProtoClassField(localName: string, protoClass: any) {
|
|||
return fieldGetter?.();
|
||||
}
|
||||
|
||||
const logger = new Logger("FakeNitro");
|
||||
|
||||
const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
|
||||
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
|
||||
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
|
||||
|
|
@ -334,20 +336,11 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
||||
replacement: [
|
||||
{
|
||||
// Call our function to decide whether the emoji link should be kept or not
|
||||
predicate: () => settings.store.transformEmojis,
|
||||
match: /(\i)=\(0,\i\.\i\)\((\i)\)(?=&&)(?<=\i=\1,\2=\(\i\?\?\i\)\.embeds.{0,50}?)/,
|
||||
replace: (m, content, _embeds) => `${m} && !$self.shouldKeepEmojiLink(${content}[0])`
|
||||
},
|
||||
{
|
||||
// Patch the rendered message content to add fake nitro emojis or remove sticker links
|
||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||
match: /(?=return{hasSpoilerEmbeds:\i,hasBailedAst:\i,content:(\i))/,
|
||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
|
||||
}
|
||||
]
|
||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||
replacement: {
|
||||
match: /\(\{ast:(\i)(?=,inline:\i)/,
|
||||
replace: "({ast:$self.transformAst($1)"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "}renderStickersAccessories(",
|
||||
|
|
@ -525,107 +518,58 @@ export default definePlugin({
|
|||
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];
|
||||
},
|
||||
|
||||
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
|
||||
// If content has more than one child or it's a single ReactElement like a header, list or span
|
||||
if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content;
|
||||
transformAst(ast: any[]) {
|
||||
try {
|
||||
if (!settings.store.transformCompoundSentence && ast.length > 1) {
|
||||
return ast;
|
||||
}
|
||||
// Filter out sticker links. These are transformed into actual stickers later, so we don't want duplicate links
|
||||
if (settings.store.transformStickers) {
|
||||
ast = ast.filter(node => {
|
||||
const { type, target } = node;
|
||||
|
||||
let nextIndex = content.length;
|
||||
if (type !== "link") return true;
|
||||
if (fakeNitroStickerRegex.test(target)) return false;
|
||||
|
||||
const gifStickerLinkMatch = target.match(fakeNitroGifStickerRegex);
|
||||
const isGifStickerLink = gifStickerLinkMatch && StickersStore.getStickerById(gifStickerLinkMatch[1]);
|
||||
|
||||
return !isGifStickerLink;
|
||||
});
|
||||
}
|
||||
|
||||
const transformLinkChild = (child: ReactElement<any>) => {
|
||||
if (settings.store.transformEmojis) {
|
||||
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
|
||||
if (fakeNitroMatch) {
|
||||
ast = ast.map(node => {
|
||||
const { type, target } = node;
|
||||
|
||||
if (type !== "link") return node;
|
||||
|
||||
const fakeNitroMatch = target.match(fakeNitroEmojiRegex);
|
||||
if (!fakeNitroMatch) return node;
|
||||
|
||||
let url: URL | null = null;
|
||||
try {
|
||||
url = new URL(child.props.href);
|
||||
url = new URL(target);
|
||||
} catch { }
|
||||
|
||||
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
|
||||
const isAnimated = fakeNitroMatch[2] === "gif" || url?.searchParams.get("animated") === "true";
|
||||
const emojiId = fakeNitroMatch[1];
|
||||
const emojiName = EmojiStore.getCustomEmojiById(emojiId)?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
|
||||
const animated = fakeNitroMatch[2] === "gif" || url?.searchParams.get("animated") === "true";
|
||||
|
||||
return Parser.defaultRules.customEmoji.react({
|
||||
jumboable: !inline && content.length === 1 && typeof content[0].type !== "string",
|
||||
animated: isAnimated,
|
||||
emojiId: fakeNitroMatch[1],
|
||||
name: emojiName,
|
||||
return {
|
||||
animated,
|
||||
emojiId,
|
||||
name: `:${emojiName}:`,
|
||||
type: "customEmoji",
|
||||
fake: true
|
||||
}, void 0, { key: String(nextIndex++) });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.store.transformStickers) {
|
||||
if (fakeNitroStickerRegex.test(child.props.href)) return null;
|
||||
|
||||
const gifMatch = child.props.href.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker
|
||||
if (StickersStore.getStickerById(gifMatch[1])) return null;
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
};
|
||||
|
||||
const transformChild = (child: ReactElement<any>) => {
|
||||
if (child?.props?.trusted != null) return transformLinkChild(child);
|
||||
if (child?.props?.children != null) {
|
||||
if (!Array.isArray(child.props.children)) {
|
||||
child.props.children = modifyChild(child.props.children);
|
||||
return child;
|
||||
}
|
||||
|
||||
child.props.children = modifyChildren(child.props.children);
|
||||
if (child.props.children.length === 0) return null;
|
||||
return child;
|
||||
}
|
||||
|
||||
return child;
|
||||
};
|
||||
|
||||
const modifyChild = (child: ReactElement<any>) => {
|
||||
const newChild = transformChild(child);
|
||||
|
||||
if (newChild?.type === "ul" || newChild?.type === "ol") {
|
||||
this.ensureChildrenIsArray(newChild);
|
||||
if (newChild.props.children.length === 0) return null;
|
||||
|
||||
let listHasAnItem = false;
|
||||
for (const [index, child] of newChild.props.children.entries()) {
|
||||
if (child == null) {
|
||||
delete newChild.props.children[index];
|
||||
continue;
|
||||
}
|
||||
|
||||
this.ensureChildrenIsArray(child);
|
||||
if (child.props.children.length > 0) listHasAnItem = true;
|
||||
else delete newChild.props.children[index];
|
||||
}
|
||||
|
||||
if (!listHasAnItem) return null;
|
||||
|
||||
newChild.props.children = this.clearEmptyArrayItems(newChild.props.children);
|
||||
}
|
||||
|
||||
return newChild;
|
||||
};
|
||||
|
||||
const modifyChildren = (children: Array<ReactElement<any>>) => {
|
||||
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
|
||||
|
||||
children = this.clearEmptyArrayItems(children);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
try {
|
||||
const newContent = modifyChildren(lodash.cloneDeep(content));
|
||||
this.trimContent(newContent);
|
||||
|
||||
return newContent;
|
||||
} catch (err) {
|
||||
new Logger("FakeNitro").error(err);
|
||||
return content;
|
||||
} catch (e) {
|
||||
logger.error("Error transforming AST:", e);
|
||||
}
|
||||
|
||||
return ast;
|
||||
},
|
||||
|
||||
patchFakeNitroStickers(stickers: Array<any>, message: Message) {
|
||||
|
|
@ -727,10 +671,6 @@ export default definePlugin({
|
|||
});
|
||||
},
|
||||
|
||||
shouldKeepEmojiLink(link: any) {
|
||||
return link.target && fakeNitroEmojiRegex.test(link.target);
|
||||
},
|
||||
|
||||
addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {
|
||||
if (!fake) return node;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# Summaries
|
||||
|
||||
Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations.
|
||||
|
||||
Read more about summaries in the [official Discord help article](https://support.discord.com/hc/en-us/articles/12926016807575-In-Channel-Conversation-Summaries)!
|
||||
|
||||
Note that this plugin can't fetch old summaries, it can only display ones created while your Discord is running with the plugin enabled.
|
||||
|
||||

|
||||
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { hasGuildFeature } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, GuildStore } from "@webpack/common";
|
||||
|
||||
const SummaryStore = findByPropsLazy("allSummaries", "findSummary");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
summaryExpiryThresholdDays: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "The time in days before a summary is removed. Note that only up to 50 summaries are kept per channel",
|
||||
markers: [1, 3, 5, 7, 10, 15, 20, 25, 30],
|
||||
stickToMarkers: false,
|
||||
default: 3,
|
||||
}
|
||||
});
|
||||
|
||||
interface Summary {
|
||||
count: number;
|
||||
end_id: string;
|
||||
id: string;
|
||||
message_ids: string[];
|
||||
people: string[];
|
||||
source: number;
|
||||
start_id: string;
|
||||
summ_short: string;
|
||||
topic: string;
|
||||
type: number;
|
||||
unsafe: boolean;
|
||||
}
|
||||
|
||||
interface ChannelSummary {
|
||||
type: string;
|
||||
channel_id: string;
|
||||
guild_id: string;
|
||||
summaries: Summary[];
|
||||
|
||||
// custom property
|
||||
time?: number;
|
||||
}
|
||||
// TODO: these types are wrong and evil and incorrect
|
||||
function createChannelSummaryFromServer(s: Summary, channelId: string): ChannelSummary {
|
||||
return {
|
||||
id: s.id,
|
||||
topic: s.topic,
|
||||
summShort: s.summ_short,
|
||||
people: Array.from(new Set(s.people)),
|
||||
startId: s.start_id,
|
||||
endId: s.end_id,
|
||||
count: s.count,
|
||||
channelId,
|
||||
source: s.source,
|
||||
type: s.type as any,
|
||||
} as any as ChannelSummary;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Summaries",
|
||||
description: "Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations",
|
||||
tags: ["Chat", "Fun"],
|
||||
authors: [Devs.mantikafasi],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "SUMMARIZEABLE.has",
|
||||
replacement: {
|
||||
match: /\i\.features\.has\(\i\.\i\.SUMMARIES_ENABLED\w+?\)/g,
|
||||
replace: "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "RECEIVE_CHANNEL_SUMMARY(",
|
||||
replacement: {
|
||||
match: /shouldFetch\((\i),\i\){/,
|
||||
replace: "$& if(!$self.shouldFetch($1)) return false;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
flux: {
|
||||
CONVERSATION_SUMMARY_UPDATE(data) {
|
||||
const incomingSummaries: ChannelSummary[] = data.summaries.map((summary: any) => ({
|
||||
...createChannelSummaryFromServer(summary, undefined!),
|
||||
time: Date.now()
|
||||
}));
|
||||
|
||||
// idk if this is good for performance but it doesnt seem to be a problem in my experience
|
||||
DataStore.update("summaries-data", summaries => {
|
||||
summaries ??= {};
|
||||
summaries[data.channel_id] ? summaries[data.channel_id].unshift(...incomingSummaries) : (summaries[data.channel_id] = incomingSummaries);
|
||||
if (summaries[data.channel_id].length > 50)
|
||||
summaries[data.channel_id] = summaries[data.channel_id].slice(0, 50);
|
||||
|
||||
return summaries;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
await DataStore.update("summaries-data", summaries => {
|
||||
summaries ??= {};
|
||||
for (const key of Object.keys(summaries)) {
|
||||
for (let i = summaries[key].length - 1; i >= 0; i--) {
|
||||
if (summaries[key][i].time < Date.now() - 1000 * 60 * 60 * 24 * settings.store.summaryExpiryThresholdDays) {
|
||||
summaries[key].splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaries[key].length === 0) {
|
||||
delete summaries[key];
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(SummaryStore.allSummaries(), summaries);
|
||||
return summaries;
|
||||
});
|
||||
},
|
||||
|
||||
shouldFetch(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
// SUMMARIES_ENABLED feature is not in discord-types
|
||||
const guild = GuildStore.getGuild(channel.guild_id);
|
||||
|
||||
return hasGuildFeature(guild, "SUMMARIES_ENABLED_GA");
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue