diff --git a/src/main/csp/index.ts b/src/main/csp/index.ts index c0d17b4a4..55c0c175e 100644 --- a/src/main/csp/index.ts +++ b/src/main/csp/index.ts @@ -55,6 +55,11 @@ export const CspPolicies: PolicyMap = { // Function Specific "api.github.com": ConnectSrc, // used for updating Vencord itself "ws.audioscrobbler.com": ConnectSrc, // Last.fm API + "musicbrainz.org": ConnectSrc, + "*.listenbrainz.org": ConnectSrc, + "coverartarchive.org": ConnectSrc, + "archive.org": ConnectSrc, + "*.archive.org": ConnectSrc, "translate-pa.googleapis.com": ConnectSrc, // Google Translate API "*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) "manti.vendicated.dev": ImageSrc, // ReviewDB API diff --git a/src/plugins/_api/messagePopover.ts b/src/plugins/_api/messagePopover.ts index ef49d5119..7f6c5b938 100644 --- a/src/plugins/_api/messagePopover.ts +++ b/src/plugins/_api/messagePopover.ts @@ -27,7 +27,7 @@ export default definePlugin({ { find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", replacement: { - match: /(?<=\]\}\)),(.{0,40}togglePopout:.+?\}\))\]\}\):null,(?<=\((\i\.\i),\{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/, + match: /(?<=\]\}\)),(.{0,40}togglePopout:.+?\}\))\]\}\):null,(?<=\((\i),\{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/, replace: (_, ReactButton, ButtonComponent, showReactButton, message) => "" + `]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,` } diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 59f65941d..da29500b1 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -274,7 +274,7 @@ export default definePlugin({ predicate: () => settings.store.closeAllHomeButton, replacement: { // Close all folders when clicking the home button - match: /(?<=onClick:(\(\)=>|function\(\)){)(?=.{0,300}"discodo")/, + match: /(?<=onClick:(?:function)?\(\)(?:=>)?{)(?=.{0,300}"discodo")/, replace: "$self.closeFolders();" } } diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 86b540df2..93b4f00fc 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -55,8 +55,8 @@ export default definePlugin({ replace: "$self.DecorationGridItem=$1;", }, { - match: /(?=function (\i)\(\i\){let{user:\i,avatarDecoration)/, - replace: "$self.DecorationGridDecoration=$1;", + match: /(?<=(?:(\i)=)?)(?:\i=>|function (\i)\(\i\)){let{user:\i,avatarDecoration/, + replace: (m, arrowFunctionName, functionName) => `$self.DecorationGridDecoration=${arrowFunctionName ?? functionName}${arrowFunctionName ? "" : ";"}${m}`, }, // Remove NEW label from decor avatar decorations { diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 747295eac..8948c9fec 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -24,8 +24,6 @@ import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; import { BaseText } from "@components/BaseText"; import { Devs } from "@utils/constants.js"; -import { classNameFactory } from "@utils/css"; -import { classes } from "@utils/misc"; import { Queue } from "@utils/Queue"; import definePlugin, { OptionType } from "@utils/types"; import { Channel, Message } from "@vencord/discord-types"; @@ -45,7 +43,6 @@ import { } from "@webpack/common"; import { ComponentType, JSX } from "react"; -const cl = classNameFactory("vc-message-link-embeds-"); const messageCache = new Map ( -
+
. */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginSetting, migratePluginSettings } from "@api/Settings"; import { LinkButton } from "@components/Button"; import { Card } from "@components/Card"; import { Heading } from "@components/Heading"; import { Margins } from "@components/margins"; import { Paragraph } from "@components/Paragraph"; import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types"; import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "@vencord/discord-types/enums"; import { ApplicationAssetUtils, AuthenticationStore, FluxDispatcher, PresenceStore } from "@webpack/common"; -interface TrackData { +import { LastFMScrobbler } from "./lastfm"; +import { ListenBrainzScrobbler } from "./listenbrainz"; + +export interface TrackData { name: string; album: string; artist: string; - url: string; - imageUrl?: string; + trackURL?: string; + artistURL?: string; + albumURL?: string; + imageURL?: string; + serviceName?: string; +} + +export interface ScrobblerBackend { + name: string, + id: string, + + fetchTrackData(username: string, apiKey?: string): Promise; + getUserURL(username: string): string; } const enum NameFormat { @@ -43,18 +56,15 @@ const enum NameFormat { SongFirst = "song-first", ArtistOnly = "artist", SongOnly = "song", - AlbumName = "album" + AlbumName = "album", + ServiceName = "service-name" } // Last.fm API keys are essentially public information and have no access to your account, so including one here is fine. -const API_KEY = "790c37d90400163a5a5fe00d6ca32ef0"; +const LASTFM_API_KEY = "790c37d90400163a5a5fe00d6ca32ef0"; const DISCORD_APP_ID = "1108588077900898414"; const LASTFM_PLACEHOLDER_IMAGE_HASH = "2a96cbd8b46e442fc41c2b86b821562f"; -const logger = new Logger("LastFMRichPresence"); - -let updateInterval: NodeJS.Timeout | undefined; - async function getApplicationAsset(key: string): Promise { return (await ApplicationAssetUtils.fetchAssetIds(DISCORD_APP_ID, [key]))[0]; } @@ -68,17 +78,32 @@ function setActivity(activity: Activity | null) { } const settings = definePluginSettings({ + scrobblerBackend: { + description: "The scrobbler backend to use.", + type: OptionType.SELECT, + options: [ + { + "label": "Last.FM", + "value": "lastfm", + "default": true + }, + { + "label": "ListenBrainz", + "value": "listenbrainz" + } + ] as const + }, apiKey: { displayName: "API Key", description: "Custom Last.fm API key. Not required but highly recommended to avoid rate limiting with our shared key", type: OptionType.STRING, }, username: { - description: "Last.fm username", + description: "Username", type: OptionType.STRING, }, shareUsername: { - description: "Show link to Last.fm profile", + description: "Show link to scrobbler profile", type: OptionType.BOOLEAN, default: false, }, @@ -88,12 +113,12 @@ const settings = definePluginSettings({ default: true, }, hideWithSpotify: { - description: "Hide Last.fm presence if spotify is running", + description: "Hide presence if Spotify is running", type: OptionType.BOOLEAN, default: true, }, hideWithActivity: { - description: "Hide Last.fm presence if you have any other presence", + description: "Hide presence if you have any other presence", type: OptionType.BOOLEAN, default: false, }, @@ -130,6 +155,10 @@ const settings = definePluginSettings({ value: NameFormat.StatusName, default: true }, + { + label: "Use music service name (falls back to custom status text)", + value: NameFormat.ServiceName + }, { label: "Use format 'artist - song'", value: NameFormat.ArtistFirst @@ -162,8 +191,8 @@ const settings = definePluginSettings({ type: OptionType.SELECT, options: [ { - label: "Use large Last.fm logo", - value: "lastfmLogo", + label: "Use large scrobbler logo", + value: "logo", default: true }, { @@ -172,30 +201,34 @@ const settings = definePluginSettings({ } ], }, - showLastFmLogo: { - displayName: "Show Last.fm Logo", - description: "Show the Last.fm logo by the album cover", + showLogo: { + displayName: "Show Scrobbler Logo", + description: "Show the scrobbler service logo by the album cover", type: OptionType.BOOLEAN, default: true, }, showAlbumCover: { - description: "Show album cover. Disabling this will display a placeholder. Useful if your Music has inappropriate art", + description: "Show album cover. Disabling this will display a placeholder. Useful if your music has inappropriate art", type: OptionType.BOOLEAN, default: true, } }); +migratePluginSettings("MusicRichPresence", "LastFMRichPresence"); +migratePluginSetting("MusicRichPresence", "showLastFmLogo", "showLogo"); export default definePlugin({ - name: "LastFMRichPresence", - description: "Little plugin for Last.fm rich presence", + name: "MusicRichPresence", + description: "Rich Presence for Last.FM/Listenbrainz", tags: ["Activity", "Media"], - authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu], + searchTerms: ["lastfm", "LastFMRichPresence"], + authors: [Devs.Rini, Devs.Ven, Devs.angelcube, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu], settings, settingsAboutComponent() { return ( + Last.FM How to create an API key Set Application name and Application description to anything and leave the rest blank. Create API Key @@ -205,54 +238,11 @@ export default definePlugin({ start() { this.updatePresence(); - updateInterval = setInterval(() => { this.updatePresence(); }, 16000); + this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); }, stop() { - clearInterval(updateInterval); - updateInterval = undefined; - }, - - async fetchTrackData(): Promise { - if (!settings.store.username) - return null; - - try { - const params = new URLSearchParams({ - method: "user.getrecenttracks", - api_key: settings.store.apiKey || API_KEY, - user: settings.store.username, - limit: "1", - format: "json" - }); - - const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!res.ok) throw `${res.status} ${res.statusText}`; - - const json = await res.json(); - if (json.error) { - logger.error("Error from Last.fm API", `${json.error}: ${json.message}`); - return null; - } - - const trackData = json.recenttracks?.track[0]; - - if (!trackData?.["@attr"]?.nowplaying) - return null; - - // why does the json api have xml structure - return { - name: trackData.name || "Unknown", - album: trackData.album["#text"], - artist: trackData.artist["#text"] || "Unknown", - url: trackData.url, - imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"] - }; - } catch (e) { - logger.error("Failed to query Last.fm API", e); - // will clear the rich presence if API fails - return null; - } + clearInterval(this.updateInterval); }, async updatePresence() { @@ -260,14 +250,17 @@ export default definePlugin({ }, getLargeImage(track: TrackData): string | undefined { - if (settings.store.showAlbumCover && track.imageUrl && !track.imageUrl.includes(LASTFM_PLACEHOLDER_IMAGE_HASH)) - return track.imageUrl; + if (settings.store.showAlbumCover && track.imageURL && !track.imageURL.includes(LASTFM_PLACEHOLDER_IMAGE_HASH)) + return track.imageURL; if (settings.store.missingArt === "placeholder") return "placeholder"; }, async getActivity(): Promise { + if (!settings.store.username) + return null; + if (settings.store.hideWithActivity) { if (PresenceStore.getActivities(AuthenticationStore.getId()).some(a => a.application_id !== DISCORD_APP_ID && a.type !== ActivityType.CUSTOM_STATUS)) { return null; @@ -281,7 +274,9 @@ export default definePlugin({ } } - const trackData = await this.fetchTrackData(); + const scrobbler = settings.store.scrobblerBackend === "lastfm" ? LastFMScrobbler : ListenBrainzScrobbler; + + const trackData = await scrobbler.fetchTrackData(settings.store.username, settings.store.apiKey || LASTFM_API_KEY); if (!trackData) return null; const largeImage = this.getLargeImage(trackData); @@ -289,22 +284,23 @@ export default definePlugin({ { large_image: await getApplicationAsset(largeImage), large_text: trackData.album || undefined, - ...(settings.store.showLastFmLogo && { - small_image: await getApplicationAsset("lastfm-small"), - small_text: "Last.fm" + ...(settings.store.showLogo && { + small_image: await getApplicationAsset(`${scrobbler.id}-small`), + small_text: scrobbler.id }), } : { - large_image: await getApplicationAsset("lastfm-large"), + large_image: await getApplicationAsset(`${scrobbler.id}-large`), large_text: trackData.album || undefined, }; const buttons: ActivityButton[] = []; - if (settings.store.shareUsername) + if (settings.store.shareUsername) { buttons.push({ - label: "Last.fm Profile", - url: `https://www.last.fm/user/${settings.store.username}`, + label: `${scrobbler.name} Profile`, + url: scrobbler.getUserURL(settings.store.username!) }); + } const statusName = (() => { switch (settings.store.nameFormat) { @@ -321,6 +317,11 @@ export default definePlugin({ .replaceAll("{artist}", trackData.artist || "") .replaceAll("{album}", trackData.album || "") .replaceAll("{title}", trackData.name || ""); + case NameFormat.ServiceName: + return trackData.serviceName || settings.store.statusName + .replaceAll("{artist}", trackData.artist || "") + .replaceAll("{album}", trackData.album || "") + .replaceAll("{title}", trackData.name || ""); default: return settings.store.statusName .replaceAll("{artist}", trackData.artist || "") @@ -353,11 +354,11 @@ export default definePlugin({ }; if (settings.store.clickableLinks) { - activity.details_url = trackData.url; - activity.state_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}`; + activity.details_url = trackData.trackURL; + activity.state_url = trackData.artistURL; if (trackData.album) { - activity.assets!.large_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}/${encodeURIComponent(trackData.album)}`; + activity.assets!.large_url = trackData.albumURL; } } diff --git a/src/plugins/musicRichPresence/lastfm.ts b/src/plugins/musicRichPresence/lastfm.ts new file mode 100644 index 000000000..dc58a1de6 --- /dev/null +++ b/src/plugins/musicRichPresence/lastfm.ts @@ -0,0 +1,63 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; + +import { ScrobblerBackend, TrackData } from "."; + +const logger = new Logger("MusicRichPresence/LastFM"); + +const url = (path: string) => `https://www.last.fm${path}`; + +export const LastFMScrobbler: ScrobblerBackend = { + name: "Last.FM", + id: "lastfm", + + async fetchTrackData(username: string, apiKey: string): Promise { + try { + const params = new URLSearchParams({ + method: "user.getrecenttracks", + api_key: apiKey, + user: username, + limit: "1", + format: "json" + }); + + const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const json = await res.json(); + if (json.error) { + logger.error("Error from Last.fm API", `${json.error}: ${json.message}`); + return null; + } + + const trackData = json.recenttracks?.track[0]; + + if (!trackData?.["@attr"]?.nowplaying) + return null; + + // why does the json api have xml structure + return { + name: trackData.name || "Unknown", + album: trackData.album["#text"], + artist: trackData.artist["#text"] || "Unknown", + trackURL: trackData.url, + artistURL: trackData.artist["#text"] ? url(`/music/${encodeURIComponent(trackData.artist["#text"])}`) : undefined, + albumURL: url(`/music/${encodeURIComponent(trackData.artist["#text"])}/${encodeURIComponent(trackData.album["#text"])}`), + imageURL: trackData.image?.find((x: any) => x.size === "large")?.["#text"] + } as TrackData; + } catch (e) { + logger.error("Failed to query Last.FM API", e); + // will clear the rich presence if API fails + return null; + } + }, + + getUserURL(username: string): string { + return url(`/user/${username}`); + } +}; diff --git a/src/plugins/musicRichPresence/listenbrainz.ts b/src/plugins/musicRichPresence/listenbrainz.ts new file mode 100644 index 000000000..6844b378e --- /dev/null +++ b/src/plugins/musicRichPresence/listenbrainz.ts @@ -0,0 +1,105 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; +import { Logger } from "@utils/Logger"; + +import { ScrobblerBackend, TrackData } from "."; + +const logger = new Logger("AudioScrobblerRichPresence/ListenBrainz"); + +const url = (path: string) => `https://listenbrainz.org${path}`; + +async function fetchCoverArt(releaseGroupMBID: string) { + const res = await fetch(`https://coverartarchive.org/release-group/${releaseGroupMBID}`); + if (!res.ok) return null; + return res.json().then(json => json.images[0].thumbnails.large); +} + +async function getUrls(additionalInfo: Record | undefined, trackName: string, artistName: string, releaseName: string): Promise> { + // Well tagged music will have MBIDs which we can use directly. These are optional but highly recommended in ListenBrainz scrobbles. + // If your music doesn't have these, it's highly recommended to use https://picard.musicbrainz.org/ to automatically add them + if (additionalInfo?.recording_mbid) { + const { release_group_mbid, release_mbid, recording_mbid, artist_mbids } = additionalInfo; + + return { + imageURL: release_group_mbid ? await fetchCoverArt(release_group_mbid) : undefined, + trackURL: recording_mbid ? url(`/track/${recording_mbid}`) : undefined, + albumURL: release_group_mbid + ? url(`/release-group/${release_group_mbid}`) + : release_mbid + ? url(`/release/${release_mbid}`) + : undefined, + artistURL: artist_mbids?.length ? url(`/artist/${artist_mbids[0]}`) : undefined, + }; + } + + // If no MBIDs are present, try to search for the track on MusicBrainz + + // this needs to be encoded separately—URLSearchParams encodes spaces as "+" + const query = encodeURIComponent(`artist:"${artistName}" AND recording:"${trackName}" AND album:${releaseName}`); + + const params = new URLSearchParams({ + fmt: "json", + limit: "1" + }); + + const metadata = await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, { + headers: { "User-Agent": VENCORD_USER_AGENT } + }) + .then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`))) + .then(json => json.recordings?.[0]); + + if (!metadata) { + return {}; + } + + const artist = metadata["artist-credit"]?.[0]?.artist; + const release = metadata.releases?.[0]; + + return { + imageURL: release?.["release-group"] ? await fetchCoverArt(release["release-group"].id) : undefined, + trackURL: url(`/track/${metadata.id}/`), + albumURL: release?.id ? url(`/release/${release.id}/`) : release?.["release-group"]?.id ? url(`/release-group/${release["release-group"].id}/`) : undefined, + artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined, + }; +} + +export const ListenBrainzScrobbler: ScrobblerBackend = { + name: "ListenBrainz", + id: "listenbrainz", + + async fetchTrackData(username: string, _apiKey?: string): Promise { + try { + const res = await fetch(`https://api.listenbrainz.org/1/user/${username}/playing-now`); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + + const data = await res.json().then(json => json.payload?.listens[0]); + if (!data?.playing_now || !data?.track_metadata) + return null; + + const { track_name, artist_name, release_name, additional_info } = data.track_metadata; + + const trackData = { + name: track_name || "Unknown", + artist: artist_name, + album: release_name || "Unknown", + serviceName: additional_info?.music_service_name || additional_info?.submission_client, + ...await getUrls(additional_info, track_name, artist_name, release_name) + } as TrackData; + + return trackData; + } catch (e) { + logger.error("Failed to query ListenBrainz API", e); + // will clear the rich presence if API fails + return null; + } + }, + + getUserURL(username: string): string { + return url(`/user/${username}`); + } +}; diff --git a/src/plugins/noUnblockToJump/index.ts b/src/plugins/noUnblockToJump/index.ts index 6c1f504b6..a9071dcdb 100644 --- a/src/plugins/noUnblockToJump/index.ts +++ b/src/plugins/noUnblockToJump/index.ts @@ -23,7 +23,7 @@ export default definePlugin({ name: "NoUnblockToJump", description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them", tags: ["Utility"], - authors: [Devs.dzshn], + authors: [Devs.Rini], patches: [ { find: "#{intl::UNIGNORE_TO_JUMP_BODY}", diff --git a/src/plugins/serverListIndicators/index.tsx b/src/plugins/serverListIndicators/index.tsx index 0d31102c9..cc082ee60 100644 --- a/src/plugins/serverListIndicators/index.tsx +++ b/src/plugins/serverListIndicators/index.tsx @@ -142,7 +142,7 @@ export default definePlugin({ name: "ServerListIndicators", description: "Add online friend count or server count in the server list", tags: ["Servers", "Appearance"], - authors: [Devs.dzshn, EquicordDevs.Panniku], + authors: [Devs.Rini, EquicordDevs.Panniku], dependencies: ["ServerListAPI"], settings, diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 5872524a8..33e7bbfad 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -83,7 +83,7 @@ export default definePlugin({ name: "ShowHiddenChannels", description: "Show channels that you do not have access to view.", tags: ["Servers", "Utility"], - authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn, EquicordDevs.Oggetto], + authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.Rini, EquicordDevs.Oggetto], isModified: true, settings, diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 2c9ebac08..51f24f94c 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -45,7 +45,7 @@ export default definePlugin({ replacement: [ { // if (inlinedCalculatePlayingCount(a,b) >= limit) return; - match: /(BURST_REACTION_EFFECT_PLAY:(\i=>|function\(\i\)){.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=5+?(?=\))/, + match: /(BURST_REACTION_EFFECT_PLAY:(?:\i=>|function\(\i\)){.+?if\()(\(?(?:function)?\(\i,\i\)(?:=>)?{.+?\(\i,\i\))>=5+?(?=\))/, replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})` } ] diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9e4a8c82f..37c602ce0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -192,10 +192,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "kemo", id: 715746190813298788n }, - dzshn: { - name: "dzshn", - id: 310449948011528192n - }, Ducko: { name: "Ducko", id: 506482395269169153n @@ -374,8 +370,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({ id: 354191516979429376n }, Rini: { - name: "Rini", - id: 1079479184478441643n + name: "rini", + id: 0n }, castdrian: { name: "castdrian", @@ -515,7 +511,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, Byeoon: { name: "byeoon", - id: 1167275288036655133n + id: 495683688474869781n }, Kaitlyn: { name: "kaitlyn", @@ -666,6 +662,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "NightmareSan", id: 304239816466235392n }, + angelcube: { + name: "angelcube", + id: 958505257288208446n + }, Lunascape: { name: "Lunascape", id: 383365021415243776n