Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen 2026-09-25 09:24:36 -04:00
commit 6eb61b0482
No known key found for this signature in database
8 changed files with 51 additions and 695 deletions

View file

@ -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": {

View file

@ -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} />

View file

@ -1,9 +0,0 @@
# AppleMusicRichPresence
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
## 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.

View file

@ -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,
};
}
});

View file

@ -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 };
}

View file

@ -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;

View file

@ -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.
![](https://github.com/Vendicated/Vencord/assets/45497981/bd931b0c-2e85-4c10-9f7c-8ba01eb55745)

View file

@ -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");
}
});