diff --git a/package.json b/package.json index 70ffb7bdf..14c54b0fe 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/settings/tabs/patchHelper/PatchPreview.tsx b/src/components/settings/tabs/patchHelper/PatchPreview.tsx index e294280da..9fde3b2a2 100644 --- a/src/components/settings/tabs/patchHelper/PatchPreview.tsx +++ b/src/components/settings/tabs/patchHelper/PatchPreview.tsx @@ -120,7 +120,7 @@ export function PatchPreview({ module, match, replacement, setReplacementError } return ( <> - Module {id} + Module {id} diff --git a/src/plugins/appleMusic.desktop/README.md b/src/plugins/appleMusic.desktop/README.md deleted file mode 100644 index 52ab93bfd..000000000 --- a/src/plugins/appleMusic.desktop/README.md +++ /dev/null @@ -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. diff --git a/src/plugins/appleMusic.desktop/index.tsx b/src/plugins/appleMusic.desktop/index.tsx deleted file mode 100644 index 3153744fb..000000000 --- a/src/plugins/appleMusic.desktop/index.tsx +++ /dev/null @@ -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; - -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 <> - - 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. - - ; - }, - - 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 { - 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, - }; - } -}); diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts deleted file mode 100644 index 8698ad426..000000000 --- a/src/plugins/appleMusic.desktop/native.ts +++ /dev/null @@ -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(//); - 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 { - 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 }; -} diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 3eeabab14..5db3183a0 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -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, 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) => { 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) => { - 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) => { - 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>) => { - 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, message: Message) { @@ -727,10 +671,6 @@ export default definePlugin({ }); }, - shouldKeepEmojiLink(link: any) { - return link.target && fakeNitroEmojiRegex.test(link.target); - }, - addFakeNotice(type: FakeNoticeType, node: Array, fake: boolean) { if (!fake) return node; diff --git a/src/plugins/seeSummaries/README.md b/src/plugins/seeSummaries/README.md deleted file mode 100644 index 1c8b6c793..000000000 --- a/src/plugins/seeSummaries/README.md +++ /dev/null @@ -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) diff --git a/src/plugins/seeSummaries/index.tsx b/src/plugins/seeSummaries/index.tsx deleted file mode 100644 index 1b5ff70c5..000000000 --- a/src/plugins/seeSummaries/index.tsx +++ /dev/null @@ -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"); - } -});