From d2a3faa915b036b5ef0ccf36c5366d1f933d5d03 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Mon, 14 Apr 2025 12:23:38 -0600 Subject: [PATCH 01/29] feat: Add slack scrobbler --- package.json | 3 + pnpm-lock.yaml | 44 ++++ src/plugins/scrobbler/index.ts | 28 +++ src/plugins/scrobbler/main.ts | 7 + src/plugins/scrobbler/menu.ts | 78 ++++++ .../scrobbler/services/slack-api-client.ts | 51 ++++ src/plugins/scrobbler/services/slack.ts | 224 ++++++++++++++++++ 7 files changed, 435 insertions(+) create mode 100644 src/plugins/scrobbler/services/slack-api-client.ts create mode 100644 src/plugins/scrobbler/services/slack.ts diff --git a/package.json b/package.json index f4a7a552df..5a83c2cbda 100644 --- a/package.json +++ b/package.json @@ -256,6 +256,7 @@ "@skyra/jaro-winkler": "1.1.1", "@xhayper/discord-rpc": "1.2.1", "async-mutex": "0.5.0", + "axios": "^1.8.4", "bgutils-js": "3.2.0", "butterchurn": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4", @@ -273,6 +274,7 @@ "fast-average-color": "9.5.0", "fast-equals": "5.2.2", "filenamify": "6.0.0", + "form-data": "^4.0.2", "hanja": "1.1.4", "happy-dom": "17.4.4", "hono": "4.7.6", @@ -311,6 +313,7 @@ "@stylistic/eslint-plugin-js": "4.2.0", "@total-typescript/ts-reset": "0.6.1", "@types/electron-localshortcut": "3.1.3", + "@types/form-data": "^2.5.2", "@types/howler": "2.2.12", "@types/html-to-text": "9.0.4", "@types/semver": "7.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1f251c800..d2770a5582 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: async-mutex: specifier: 0.5.0 version: 0.5.0 + axios: + specifier: ^1.8.4 + version: 1.8.4 bgutils-js: specifier: 3.2.0 version: 3.2.0 @@ -135,6 +138,9 @@ importers: filenamify: specifier: 6.0.0 version: 6.0.0 + form-data: + specifier: ^4.0.2 + version: 4.0.2 hanja: specifier: 1.1.4 version: 1.1.4 @@ -244,6 +250,9 @@ importers: '@types/electron-localshortcut': specifier: 3.1.3 version: 3.1.3 + '@types/form-data': + specifier: ^2.5.2 + version: 2.5.2 '@types/howler': specifier: 2.2.12 version: 2.2.12 @@ -1233,6 +1242,10 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/form-data@2.5.2': + resolution: {integrity: sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==} + deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed. + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -1596,6 +1609,9 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + babel-plugin-jsx-dom-expressions@0.39.7: resolution: {integrity: sha512-8GzVmFla7jaTNWW8W+lTMl9YGva4/06CtwJjySnkYtt8G1v9weCzc2SuF1DfrudcCNb2Doetc1FRg33swBYZCA==} peerDependencies: @@ -2487,6 +2503,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -3738,6 +3763,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -5614,6 +5642,10 @@ snapshots: '@types/estree@1.0.7': {} + '@types/form-data@2.5.2': + dependencies: + form-data: 4.0.2 + '@types/fs-extra@9.0.13': dependencies: '@types/node': 22.13.5 @@ -6018,6 +6050,14 @@ snapshots: await-to-js@3.0.0: {} + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -7183,6 +7223,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -8440,6 +8482,8 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + proxy-from-env@1.1.0: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 diff --git a/src/plugins/scrobbler/index.ts b/src/plugins/scrobbler/index.ts index ceed9d7776..5cc0d9b0d8 100644 --- a/src/plugins/scrobbler/index.ts +++ b/src/plugins/scrobbler/index.ts @@ -71,6 +71,28 @@ export interface ScrobblerPluginConfig { */ apiRoot: string; }; + slack: { + /** + * Enable Slack scrobbling + * + * @default false + */ + enabled: boolean; + /** + * Slack OAuth token + */ + token: string | undefined; + /** + * Slack cookie token (d cookie value) + */ + cookieToken: string | undefined; + /** + * Name to use for the custom emoji in Slack + * + * @default 'my-album-art' + */ + emojiName: string; + }; }; } @@ -92,6 +114,12 @@ export const defaultConfig: ScrobblerPluginConfig = { token: undefined, apiRoot: 'https://api.listenbrainz.org/1/', }, + slack: { + enabled: false, + token: undefined, + cookieToken: undefined, + emojiName: 'my-album-art', + }, }, }; diff --git a/src/plugins/scrobbler/main.ts b/src/plugins/scrobbler/main.ts index 91a02a75ef..2f8b1aa242 100644 --- a/src/plugins/scrobbler/main.ts +++ b/src/plugins/scrobbler/main.ts @@ -9,6 +9,7 @@ import { createBackend } from '@/utils'; import { LastFmScrobbler } from './services/lastfm'; import { ListenbrainzScrobbler } from './services/listenbrainz'; +import { SlackScrobbler } from './services/slack'; import type { ScrobblerPluginConfig } from './index'; import type { ScrobblerBase } from './services/base'; @@ -51,6 +52,12 @@ export const backend = createBackend< } else { this.enabledScrobblers.delete('listenbrainz'); } + + if (config.scrobblers.slack && config.scrobblers.slack.enabled) { + this.enabledScrobblers.set('slack', new SlackScrobbler(window)); + } else { + this.enabledScrobblers.delete('slack'); + } }, async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) { diff --git a/src/plugins/scrobbler/menu.ts b/src/plugins/scrobbler/menu.ts index 08a5702d64..29020a4137 100644 --- a/src/plugins/scrobbler/menu.ts +++ b/src/plugins/scrobbler/menu.ts @@ -79,6 +79,63 @@ async function promptListenbrainzOptions( } } +async function promptSlackOptions( + options: ScrobblerPluginConfig, + setConfig: SetConfType, + window: BrowserWindow, +) { + const output = await prompt( + { + title: 'Slack Settings', + label: 'Slack Settings', + type: 'multiInput', + multiInputOptions: [ + { + label: 'Slack OAuth Token', + value: options.scrobblers.slack?.token, + inputAttrs: { + type: 'text', + }, + }, + { + label: 'Slack Cookie Token (d cookie value)', + value: options.scrobblers.slack?.cookieToken, + inputAttrs: { + type: 'text', + }, + }, + { + label: 'Emoji Name (for album art upload)', + value: options.scrobblers.slack?.emojiName, + inputAttrs: { + type: 'text', + }, + }, + ], + resizable: true, + height: 360, + ...promptOptions(), + }, + window, + ); + + if (output) { + if (output[0]) { + options.scrobblers.slack.token = output[0]; + } + + if (output[1]) { + options.scrobblers.slack.cookieToken = output[1]; + } + + if (output[2]) { + options.scrobblers.slack.emojiName = output[2]; + } + + setConfig(options); + } +} + export const onMenu = async ({ window, getConfig, @@ -147,5 +204,26 @@ export const onMenu = async ({ }, ], }, + { + label: 'Slack', + submenu: [ + { + label: t('main.menu.plugins.enabled'), + type: 'checkbox', + checked: Boolean(config.scrobblers.slack?.enabled), + click(item) { + backend.toggleScrobblers(config, window); + config.scrobblers.slack.enabled = item.checked; + setConfig(config); + }, + }, + { + label: 'Slack Settings', + click() { + promptSlackOptions(config, setConfig, window); + }, + }, + ], + }, ]; }; diff --git a/src/plugins/scrobbler/services/slack-api-client.ts b/src/plugins/scrobbler/services/slack-api-client.ts new file mode 100644 index 0000000000..3b6e82bf2a --- /dev/null +++ b/src/plugins/scrobbler/services/slack-api-client.ts @@ -0,0 +1,51 @@ +import axios, { AxiosResponse } from 'axios'; + +/** + * Centralized Slack API client for all requests + */ +export class SlackApiClient { + readonly token: string; + readonly cookie: string; + + constructor(token: string, cookie: string) { + this.token = token; + this.cookie = cookie; + } + + private getBaseHeaders(): Record { + return { + 'Cookie': `d=${this.cookie}`, + }; + } + + /** + * POST to a Slack API endpoint + */ + async post(endpoint: string, data: any, formData = false): Promise { + const url = `https://slack.com/api/${endpoint}`; + let headers = this.getBaseHeaders(); + let payload = data; + if (formData) { + headers = { ...headers, ...data.getHeaders() }; + } else { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + payload = new URLSearchParams(data).toString(); + } + return axios.post(url, payload, { headers, maxBodyLength: Infinity, validateStatus: () => true }); + } + + /** + * GET from a Slack API endpoint + */ + async get(endpoint: string, params: Record = {}): Promise { + const url = `https://slack.com/api/${endpoint}`; + const headers = this.getBaseHeaders(); + return axios.get(url, { headers, params, validateStatus: () => true }); + } +} + +export interface SlackApiResponse { + ok: boolean; + error?: string; + [key: string]: any; +} diff --git a/src/plugins/scrobbler/services/slack.ts b/src/plugins/scrobbler/services/slack.ts new file mode 100644 index 0000000000..4e86c1f902 --- /dev/null +++ b/src/plugins/scrobbler/services/slack.ts @@ -0,0 +1,224 @@ +import { BrowserWindow, net } from 'electron'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { SlackApiClient, SlackApiResponse } from './slack-api-client'; +import FormData from 'form-data'; + +import { ScrobblerBase } from './base'; + +import type { ScrobblerPluginConfig } from '../index'; +import type { SetConfType } from '../main'; +import type { SongInfo } from '@/providers/song-info'; + +interface SlackProfileData { + status_text: string; + status_emoji: string; + status_expiration?: number; +} + +interface SlackProfileUpdateData { + token: string; + profile: string; // JSON stringified profile data +} + +/** + * SlackScrobbler: Handles Slack status and emoji updates for the scrobbler plugin + */ +export class SlackScrobbler extends ScrobblerBase { + mainWindow: BrowserWindow; + defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; + + constructor(mainWindow: BrowserWindow) { + super(); + this.mainWindow = mainWindow; + } + + /** + * Validates that all required Slack config values are present. + */ + private static validateConfig(config: ScrobblerPluginConfig): asserts config is ScrobblerPluginConfig & { + scrobblers: { slack: { token: string; cookieToken: string; emojiName: string } } + } { + const slack = config.scrobblers.slack; + if (!slack.token || !slack.cookieToken || !slack.emojiName) { + throw new Error('Missing Slack config values'); + } + } + + override isSessionCreated(config: ScrobblerPluginConfig): boolean { + try { + SlackScrobbler.validateConfig(config); + return true; + } catch { + return false; + } + } + + override async createSession( + config: ScrobblerPluginConfig, + setConfig: SetConfType, + ): Promise { + // Session creation is not required for Slack + setConfig(config); + return config; + } + + override setNowPlaying( + songInfo: SongInfo, + config: ScrobblerPluginConfig, + _setConfig: SetConfType, + ): void { + if (!this.isSessionCreated(config)) return; + const title = config.alternativeTitles && songInfo.alternativeTitle !== undefined + ? songInfo.alternativeTitle + : songInfo.title; + const artistPart = songInfo.artist || 'Unknown Artist'; + const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; + let statusText = `Now Playing: ${truncatedArtist} - ${title}`; + if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; + // Calculate expiration time (current time + remaining song duration) + const elapsed = songInfo.elapsedSeconds ?? 0; + const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); + const expirationTime = Math.floor(Date.now() / 1000) + remaining; + this.updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); + } + + override addScrobble( + _songInfo: SongInfo, + _config: ScrobblerPluginConfig, + _setConfig: SetConfType, + ): void { + // No action needed; status is managed by setNowPlaying + } + + /** + * Deletes an existing custom emoji from Slack. + */ + private async deleteExistingEmoji(config: ScrobblerPluginConfig): Promise { + SlackScrobbler.validateConfig(config); + const slack = config.scrobblers.slack; + const client = new SlackApiClient(slack.token, slack.cookieToken); + const data = { token: slack.token, name: slack.emojiName }; + const res = await client.post('emoji.remove', data); + const json = res.data as SlackApiResponse; + if (json.ok || json.error === 'emoji_not_found') return true; + console.error(`[SlackScrobbler] Error deleting emoji: ${json.error}`); + return false; + } + + /** + * Ensures the custom emoji does not exist (deletes if present). + */ + private async ensureEmojiDoesNotExist(config: ScrobblerPluginConfig): Promise { + SlackScrobbler.validateConfig(config); + const slack = config.scrobblers.slack; + const client = new SlackApiClient(slack.token, slack.cookieToken); + const res = await client.get('emoji.list', { token: slack.token }); + const json = res.data as SlackApiResponse; + if (json.ok) { + if (json.emoji && json.emoji[slack.emojiName]) { + return await this.deleteExistingEmoji(config); + } else { + return true; + } + } else { + console.error(`[SlackScrobbler] Error checking emoji list: ${json.error}`); + return false; + } + } + + /** + * Uploads a new custom emoji to Slack. + */ + private async uploadEmojiToSlack(filePath: string, config: ScrobblerPluginConfig): Promise { + SlackScrobbler.validateConfig(config); + const slack = config.scrobblers.slack; + const client = new SlackApiClient(slack.token, slack.cookieToken); + const emojiDeleted = await this.ensureEmojiDoesNotExist(config); + if (!emojiDeleted) return false; + const formData = new FormData(); + formData.append('token', slack.token); + formData.append('mode', 'data'); + formData.append('name', slack.emojiName); + const fileBuffer = fs.readFileSync(filePath); + formData.append('image', fileBuffer, { + filename: 'album-art.jpg', + contentType: 'image/jpeg', + }); + const res = await client.post('emoji.add', formData, true); + const json = res.data as SlackApiResponse; + if (json.ok) return true; + console.error(`[SlackScrobbler] Error uploading emoji: ${json.error}`); + return false; + } + + /** + * Sets the user's Slack status with the current track and emoji. + */ + private async updateSlackStatusWithEmoji( + statusText: string, + expirationTime: number, + songInfo: SongInfo, + config: ScrobblerPluginConfig, + ): Promise { + SlackScrobbler.validateConfig(config); + const slack = config.scrobblers.slack; + const client = new SlackApiClient(slack.token, slack.cookieToken); + const statusEmoji = await this.getStatusEmoji(songInfo, config); + const profileData: SlackProfileData = { + status_text: statusText, + status_emoji: statusEmoji, + status_expiration: expirationTime, + }; + const postData: SlackProfileUpdateData = { + token: slack.token, + profile: JSON.stringify(profileData), + }; + const res = await client.post('users.profile.set', postData); + const json = res.data as SlackApiResponse; + if (!json.ok) { + console.error(`Slack API error: ${json.error}`); + } + } + + /** + * Saves album art to a temporary file for emoji upload. + */ + private async saveAlbumArtToFile(songInfo: SongInfo): Promise { + if (!songInfo.imageSrc) return null; + try { + const tempDir = os.tmpdir(); + const filePath = path.join(tempDir, 'album-art.jpg'); + const response = await net.fetch(songInfo.imageSrc); + const imageBuffer = Buffer.from(await response.arrayBuffer()); + fs.writeFileSync(filePath, imageBuffer); + return filePath; + } catch (error) { + console.error('Error saving album art to file:', error); + return null; + } + } + + /** + * Gets the emoji to use for the current status (uploads album art if possible). + */ + private async getStatusEmoji(songInfo: SongInfo, config: ScrobblerPluginConfig): Promise { + if (songInfo.imageSrc) { + const filePath = await this.saveAlbumArtToFile(songInfo); + if (filePath) { + const uploaded = await this.uploadEmojiToSlack(filePath, config); + if (uploaded) { + return `:${config.scrobblers.slack.emojiName}:`; + } + } + } + return this.getDefaultEmoji(); + } + + private getDefaultEmoji(): string { + // Return a random default emoji + const randomIndex = Math.floor(Math.random() * this.defaultEmojis.length); + return this.defaultEmojis[randomIndex]; + } +} From d023aaf7face10d277e2bea42d5c7e302997082b Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 09:51:53 -0600 Subject: [PATCH 02/29] (feat) Move Slack Now Playing to its own plugin --- src/plugins/scrobbler/index.ts | 7 +- src/plugins/scrobbler/main.ts | 7 +- src/plugins/scrobbler/menu.ts | 78 +---- src/plugins/scrobbler/services/slack.ts | 224 -------------- src/plugins/slack-now-playing/index.ts | 29 ++ src/plugins/slack-now-playing/main.ts | 277 ++++++++++++++++++ src/plugins/slack-now-playing/menu.ts | 80 +++++ .../slack-api-client.ts | 0 8 files changed, 389 insertions(+), 313 deletions(-) delete mode 100644 src/plugins/scrobbler/services/slack.ts create mode 100644 src/plugins/slack-now-playing/index.ts create mode 100644 src/plugins/slack-now-playing/main.ts create mode 100644 src/plugins/slack-now-playing/menu.ts rename src/plugins/{scrobbler/services => slack-now-playing}/slack-api-client.ts (100%) diff --git a/src/plugins/scrobbler/index.ts b/src/plugins/scrobbler/index.ts index 5cc0d9b0d8..ef6266cc66 100644 --- a/src/plugins/scrobbler/index.ts +++ b/src/plugins/scrobbler/index.ts @@ -114,12 +114,7 @@ export const defaultConfig: ScrobblerPluginConfig = { token: undefined, apiRoot: 'https://api.listenbrainz.org/1/', }, - slack: { - enabled: false, - token: undefined, - cookieToken: undefined, - emojiName: 'my-album-art', - }, + }, }; diff --git a/src/plugins/scrobbler/main.ts b/src/plugins/scrobbler/main.ts index 2f8b1aa242..2e356968ce 100644 --- a/src/plugins/scrobbler/main.ts +++ b/src/plugins/scrobbler/main.ts @@ -9,7 +9,6 @@ import { createBackend } from '@/utils'; import { LastFmScrobbler } from './services/lastfm'; import { ListenbrainzScrobbler } from './services/listenbrainz'; -import { SlackScrobbler } from './services/slack'; import type { ScrobblerPluginConfig } from './index'; import type { ScrobblerBase } from './services/base'; @@ -53,11 +52,7 @@ export const backend = createBackend< this.enabledScrobblers.delete('listenbrainz'); } - if (config.scrobblers.slack && config.scrobblers.slack.enabled) { - this.enabledScrobblers.set('slack', new SlackScrobbler(window)); - } else { - this.enabledScrobblers.delete('slack'); - } + }, async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) { diff --git a/src/plugins/scrobbler/menu.ts b/src/plugins/scrobbler/menu.ts index 29020a4137..3dd75b26e4 100644 --- a/src/plugins/scrobbler/menu.ts +++ b/src/plugins/scrobbler/menu.ts @@ -79,62 +79,6 @@ async function promptListenbrainzOptions( } } -async function promptSlackOptions( - options: ScrobblerPluginConfig, - setConfig: SetConfType, - window: BrowserWindow, -) { - const output = await prompt( - { - title: 'Slack Settings', - label: 'Slack Settings', - type: 'multiInput', - multiInputOptions: [ - { - label: 'Slack OAuth Token', - value: options.scrobblers.slack?.token, - inputAttrs: { - type: 'text', - }, - }, - { - label: 'Slack Cookie Token (d cookie value)', - value: options.scrobblers.slack?.cookieToken, - inputAttrs: { - type: 'text', - }, - }, - { - label: 'Emoji Name (for album art upload)', - value: options.scrobblers.slack?.emojiName, - inputAttrs: { - type: 'text', - }, - }, - ], - resizable: true, - height: 360, - ...promptOptions(), - }, - window, - ); - - if (output) { - if (output[0]) { - options.scrobblers.slack.token = output[0]; - } - - if (output[1]) { - options.scrobblers.slack.cookieToken = output[1]; - } - - if (output[2]) { - options.scrobblers.slack.emojiName = output[2]; - } - - setConfig(options); - } -} export const onMenu = async ({ window, @@ -204,26 +148,6 @@ export const onMenu = async ({ }, ], }, - { - label: 'Slack', - submenu: [ - { - label: t('main.menu.plugins.enabled'), - type: 'checkbox', - checked: Boolean(config.scrobblers.slack?.enabled), - click(item) { - backend.toggleScrobblers(config, window); - config.scrobblers.slack.enabled = item.checked; - setConfig(config); - }, - }, - { - label: 'Slack Settings', - click() { - promptSlackOptions(config, setConfig, window); - }, - }, - ], - }, + ]; }; diff --git a/src/plugins/scrobbler/services/slack.ts b/src/plugins/scrobbler/services/slack.ts deleted file mode 100644 index 4e86c1f902..0000000000 --- a/src/plugins/scrobbler/services/slack.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { BrowserWindow, net } from 'electron'; -import fs from 'node:fs'; -import path from 'node:path'; -import os from 'node:os'; -import { SlackApiClient, SlackApiResponse } from './slack-api-client'; -import FormData from 'form-data'; - -import { ScrobblerBase } from './base'; - -import type { ScrobblerPluginConfig } from '../index'; -import type { SetConfType } from '../main'; -import type { SongInfo } from '@/providers/song-info'; - -interface SlackProfileData { - status_text: string; - status_emoji: string; - status_expiration?: number; -} - -interface SlackProfileUpdateData { - token: string; - profile: string; // JSON stringified profile data -} - -/** - * SlackScrobbler: Handles Slack status and emoji updates for the scrobbler plugin - */ -export class SlackScrobbler extends ScrobblerBase { - mainWindow: BrowserWindow; - defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; - - constructor(mainWindow: BrowserWindow) { - super(); - this.mainWindow = mainWindow; - } - - /** - * Validates that all required Slack config values are present. - */ - private static validateConfig(config: ScrobblerPluginConfig): asserts config is ScrobblerPluginConfig & { - scrobblers: { slack: { token: string; cookieToken: string; emojiName: string } } - } { - const slack = config.scrobblers.slack; - if (!slack.token || !slack.cookieToken || !slack.emojiName) { - throw new Error('Missing Slack config values'); - } - } - - override isSessionCreated(config: ScrobblerPluginConfig): boolean { - try { - SlackScrobbler.validateConfig(config); - return true; - } catch { - return false; - } - } - - override async createSession( - config: ScrobblerPluginConfig, - setConfig: SetConfType, - ): Promise { - // Session creation is not required for Slack - setConfig(config); - return config; - } - - override setNowPlaying( - songInfo: SongInfo, - config: ScrobblerPluginConfig, - _setConfig: SetConfType, - ): void { - if (!this.isSessionCreated(config)) return; - const title = config.alternativeTitles && songInfo.alternativeTitle !== undefined - ? songInfo.alternativeTitle - : songInfo.title; - const artistPart = songInfo.artist || 'Unknown Artist'; - const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; - let statusText = `Now Playing: ${truncatedArtist} - ${title}`; - if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; - // Calculate expiration time (current time + remaining song duration) - const elapsed = songInfo.elapsedSeconds ?? 0; - const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); - const expirationTime = Math.floor(Date.now() / 1000) + remaining; - this.updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); - } - - override addScrobble( - _songInfo: SongInfo, - _config: ScrobblerPluginConfig, - _setConfig: SetConfType, - ): void { - // No action needed; status is managed by setNowPlaying - } - - /** - * Deletes an existing custom emoji from Slack. - */ - private async deleteExistingEmoji(config: ScrobblerPluginConfig): Promise { - SlackScrobbler.validateConfig(config); - const slack = config.scrobblers.slack; - const client = new SlackApiClient(slack.token, slack.cookieToken); - const data = { token: slack.token, name: slack.emojiName }; - const res = await client.post('emoji.remove', data); - const json = res.data as SlackApiResponse; - if (json.ok || json.error === 'emoji_not_found') return true; - console.error(`[SlackScrobbler] Error deleting emoji: ${json.error}`); - return false; - } - - /** - * Ensures the custom emoji does not exist (deletes if present). - */ - private async ensureEmojiDoesNotExist(config: ScrobblerPluginConfig): Promise { - SlackScrobbler.validateConfig(config); - const slack = config.scrobblers.slack; - const client = new SlackApiClient(slack.token, slack.cookieToken); - const res = await client.get('emoji.list', { token: slack.token }); - const json = res.data as SlackApiResponse; - if (json.ok) { - if (json.emoji && json.emoji[slack.emojiName]) { - return await this.deleteExistingEmoji(config); - } else { - return true; - } - } else { - console.error(`[SlackScrobbler] Error checking emoji list: ${json.error}`); - return false; - } - } - - /** - * Uploads a new custom emoji to Slack. - */ - private async uploadEmojiToSlack(filePath: string, config: ScrobblerPluginConfig): Promise { - SlackScrobbler.validateConfig(config); - const slack = config.scrobblers.slack; - const client = new SlackApiClient(slack.token, slack.cookieToken); - const emojiDeleted = await this.ensureEmojiDoesNotExist(config); - if (!emojiDeleted) return false; - const formData = new FormData(); - formData.append('token', slack.token); - formData.append('mode', 'data'); - formData.append('name', slack.emojiName); - const fileBuffer = fs.readFileSync(filePath); - formData.append('image', fileBuffer, { - filename: 'album-art.jpg', - contentType: 'image/jpeg', - }); - const res = await client.post('emoji.add', formData, true); - const json = res.data as SlackApiResponse; - if (json.ok) return true; - console.error(`[SlackScrobbler] Error uploading emoji: ${json.error}`); - return false; - } - - /** - * Sets the user's Slack status with the current track and emoji. - */ - private async updateSlackStatusWithEmoji( - statusText: string, - expirationTime: number, - songInfo: SongInfo, - config: ScrobblerPluginConfig, - ): Promise { - SlackScrobbler.validateConfig(config); - const slack = config.scrobblers.slack; - const client = new SlackApiClient(slack.token, slack.cookieToken); - const statusEmoji = await this.getStatusEmoji(songInfo, config); - const profileData: SlackProfileData = { - status_text: statusText, - status_emoji: statusEmoji, - status_expiration: expirationTime, - }; - const postData: SlackProfileUpdateData = { - token: slack.token, - profile: JSON.stringify(profileData), - }; - const res = await client.post('users.profile.set', postData); - const json = res.data as SlackApiResponse; - if (!json.ok) { - console.error(`Slack API error: ${json.error}`); - } - } - - /** - * Saves album art to a temporary file for emoji upload. - */ - private async saveAlbumArtToFile(songInfo: SongInfo): Promise { - if (!songInfo.imageSrc) return null; - try { - const tempDir = os.tmpdir(); - const filePath = path.join(tempDir, 'album-art.jpg'); - const response = await net.fetch(songInfo.imageSrc); - const imageBuffer = Buffer.from(await response.arrayBuffer()); - fs.writeFileSync(filePath, imageBuffer); - return filePath; - } catch (error) { - console.error('Error saving album art to file:', error); - return null; - } - } - - /** - * Gets the emoji to use for the current status (uploads album art if possible). - */ - private async getStatusEmoji(songInfo: SongInfo, config: ScrobblerPluginConfig): Promise { - if (songInfo.imageSrc) { - const filePath = await this.saveAlbumArtToFile(songInfo); - if (filePath) { - const uploaded = await this.uploadEmojiToSlack(filePath, config); - if (uploaded) { - return `:${config.scrobblers.slack.emojiName}:`; - } - } - } - return this.getDefaultEmoji(); - } - - private getDefaultEmoji(): string { - // Return a random default emoji - const randomIndex = Math.floor(Math.random() * this.defaultEmojis.length); - return this.defaultEmojis[randomIndex]; - } -} diff --git a/src/plugins/slack-now-playing/index.ts b/src/plugins/slack-now-playing/index.ts new file mode 100644 index 0000000000..7c80523934 --- /dev/null +++ b/src/plugins/slack-now-playing/index.ts @@ -0,0 +1,29 @@ +import { createPlugin } from '@/utils'; +import { onMenu } from './menu'; +import type { SlackNowPlayingConfig, SongInfo } from './main'; +import { SlackNowPlaying } from './main'; + +export const defaultConfig: SlackNowPlayingConfig = { + enabled: false, + token: '', + cookieToken: '', + emojiName: 'my-album-art' +}; + +import { createPlugin } from '@/utils'; +import { onMenu } from './menu'; +import { backend, SlackNowPlayingConfig } from './main'; + +export default createPlugin({ + name: () => 'Slack Now Playing', + description: () => 'Sets your Slack status to the currently playing song.', + restartNeeded: true, + config: { + enabled: false, + token: '', + cookieToken: '', + emojiName: 'my-album-art', + } as SlackNowPlayingConfig, + menu: onMenu, + backend, +}); diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts new file mode 100644 index 0000000000..baf0a8937b --- /dev/null +++ b/src/plugins/slack-now-playing/main.ts @@ -0,0 +1,277 @@ +import { net } from 'electron'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { SlackApiClient, SlackApiResponse } from './slack-api-client'; +import FormData from 'form-data'; +import { createBackend, LoggerPrefix } from '@/utils'; +import registerCallback, { SongInfoEvent } from '@/providers/song-info'; + +// Import SongInfo type from provider instead of defining our own +import type { SongInfo } from '@/providers/song-info'; + +// Plugin config type +export interface SlackNowPlayingConfig { + enabled: boolean; + token: string; + cookieToken: string; + emojiName: string; + alternativeTitles?: boolean; +} + +interface SlackProfileData { + status_text: string; + status_emoji: string; + status_expiration?: number; +} + +interface SlackProfileUpdateData { + token: string; + profile: string; // JSON stringified profile data +} + + + +export interface SlackNowPlayingConfig { + enabled: boolean; + token: string; + cookieToken: string; + emojiName: string; +} + +const defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; + +const state = { + lastStatus: '', + lastEmoji: '', + window: undefined as Electron.BrowserWindow | undefined, +}; + +function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackNowPlayingConfig { + console.log(LoggerPrefix, '[SlackNowPlaying] Validating config'); + if (!config.token || !config.cookieToken || !config.emojiName) { + console.error(LoggerPrefix, '[SlackNowPlaying] Missing required config values'); + throw new Error('Missing Slack config values'); + } + console.log(LoggerPrefix, '[SlackNowPlaying] Config validation successful'); +} + +async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { + console.log(LoggerPrefix, '[SlackNowPlaying] Setting now playing status for:', songInfo.title); + + try { + validateConfig(config); + + // Skip if song is paused + if (songInfo.isPaused) { + console.log(LoggerPrefix, '[SlackNowPlaying] Song is paused, not updating status'); + return; + } + + const title = songInfo.alternativeTitle ?? songInfo.title; + const artistPart = songInfo.artist || 'Unknown Artist'; + const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; + let statusText = `Now Playing: ${truncatedArtist} - ${title}`; + if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; + + console.log(LoggerPrefix, `[SlackNowPlaying] Status text: "${statusText}"`); + + // Calculate expiration time (current time + remaining song duration) + const elapsed = songInfo.elapsedSeconds ?? 0; + const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); + const expirationTime = Math.floor(Date.now() / 1000) + remaining; + + console.log(LoggerPrefix, `[SlackNowPlaying] Expiration time: ${new Date(expirationTime * 1000).toLocaleTimeString()}`); + + await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); + } catch (error) { + console.error(LoggerPrefix, '[SlackNowPlaying] Error setting now playing status:', error); + } +} + +async function updateSlackStatusWithEmoji( + statusText: string, + expirationTime: number, + songInfo: SongInfo, + config: SlackNowPlayingConfig, +) { + console.log(LoggerPrefix, '[SlackNowPlaying] Updating Slack status with emoji'); + + try { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + + console.log(LoggerPrefix, '[SlackNowPlaying] Getting status emoji'); + const statusEmoji = await getStatusEmoji(songInfo, config); + console.log(LoggerPrefix, `[SlackNowPlaying] Using emoji: ${statusEmoji}`); + + const profileData = { + status_text: statusText, + status_emoji: statusEmoji, + status_expiration: expirationTime, + }; + + const postData = { + token: config.token, + profile: JSON.stringify(profileData), + }; + + console.log(LoggerPrefix, '[SlackNowPlaying] Sending request to Slack API'); + const res = await client.post('users.profile.set', postData); + const json = res.data as SlackApiResponse; + + if (!json.ok) { + console.error(LoggerPrefix, `[SlackNowPlaying] Slack API error: ${json.error}`); + } else { + console.log(LoggerPrefix, '[SlackNowPlaying] Successfully updated Slack status'); + state.lastStatus = statusText; + state.lastEmoji = statusEmoji; + } + } catch (error) { + console.error(LoggerPrefix, '[SlackNowPlaying] Error updating Slack status:', error); + } +} + +async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { + if (songInfo.imageSrc) { + const emojiUploaded = await uploadEmojiToSlack(songInfo, config); + if (emojiUploaded) { + return `:${config.emojiName}:`; + } + } + return defaultEmojis[Math.floor(Math.random() * defaultEmojis.length)]; +} + +async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + const filePath = await saveAlbumArtToFile(songInfo); + if (!filePath) return false; + const emojiDeleted = await ensureEmojiDoesNotExist(config); + if (!emojiDeleted) return false; + const formData = new FormData(); + formData.append('token', config.token); + formData.append('mode', 'data'); + formData.append('name', config.emojiName); + const fileBuffer = fs.readFileSync(filePath); + formData.append('image', fileBuffer, { + filename: 'album-art.jpg', + contentType: 'image/jpeg', + }); + const res = await client.post('emoji.add', formData, true); + const json = res.data as SlackApiResponse; + if (json.ok) return true; + console.error(`[SlackNowPlaying] Error uploading emoji: ${json.error}`); + return false; +} + +async function saveAlbumArtToFile(songInfo: SongInfo): Promise { + if (!songInfo.imageSrc) return null; + try { + const tempDir = os.tmpdir(); + const filePath = path.join(tempDir, 'album-art.jpg'); + const response = await net.fetch(songInfo.imageSrc); + const imageBuffer = Buffer.from(await response.arrayBuffer()); + fs.writeFileSync(filePath, imageBuffer); + return filePath; + } catch (error) { + console.error('[SlackNowPlaying] Error saving album art to file:', error); + return null; + } +} + +async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + const res = await client.get('emoji.list', { token: config.token }); + const json = res.data as SlackApiResponse; + if (json.ok) { + if (json.emoji && json.emoji[config.emojiName]) { + return await deleteExistingEmoji(config); + } else { + return true; + } + } else { + console.error(`[SlackNowPlaying] Error checking emoji list: ${json.error}`); + return false; + } +} + +async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + const data = { token: config.token, name: config.emojiName }; + const res = await client.post('emoji.remove', data); + const json = res.data as SlackApiResponse; + if (json.ok || json.error === 'emoji_not_found') return true; + console.error(`[SlackNowPlaying] Error deleting emoji: ${json.error}`); + return false; +} + +export const backend = createBackend({ + async start(ctx) { + console.log(LoggerPrefix, '[SlackNowPlaying] Starting plugin'); + state.window = ctx.window; + + // Get the config + const config = await ctx.getConfig(); + console.log(LoggerPrefix, '[SlackNowPlaying] Config loaded:', config.enabled ? 'enabled' : 'disabled'); + + // Register callback to listen for song changes + registerCallback((songInfo, event) => { + // Skip time change events + if (event === SongInfoEvent.TimeChanged) return; + + console.log(LoggerPrefix, `[SlackNowPlaying] Song info event: ${event}`); + + // Only update if plugin is enabled + if (!config.enabled) { + console.log(LoggerPrefix, '[SlackNowPlaying] Plugin is disabled, not updating status'); + return; + } + + // Update Slack status with current song + try { + // Check if config has the expected structure + const slackConfig = config as any; + if (!slackConfig || typeof slackConfig !== 'object') { + console.error(LoggerPrefix, '[SlackNowPlaying] Invalid config object'); + return; + } + + // Log the config for debugging + console.log(LoggerPrefix, '[SlackNowPlaying] Config:', JSON.stringify({ + enabled: slackConfig.enabled, + hasToken: Boolean(slackConfig.token), + hasCookieToken: Boolean(slackConfig.cookieToken), + hasEmojiName: Boolean(slackConfig.emojiName) + })); + + // Make sure the config has the required properties + if (!slackConfig.token || !slackConfig.cookieToken || !slackConfig.emojiName) { + console.error(LoggerPrefix, '[SlackNowPlaying] Missing required config values'); + return; + } + + console.log(LoggerPrefix, '[SlackNowPlaying] Updating status with song:', songInfo.title); + setNowPlaying(songInfo, slackConfig) + .catch(error => console.error(LoggerPrefix, '[SlackNowPlaying] Error in callback:', error)); + } catch (error) { + console.error(LoggerPrefix, '[SlackNowPlaying] Error processing song info:', error); + } + }); + + console.log(LoggerPrefix, '[SlackNowPlaying] Registered song info callback'); + }, + + async stop() { + console.log(LoggerPrefix, '[SlackNowPlaying] Stopping plugin'); + state.window = undefined; + // Note: We don't unregister the callback as there's no API for that + // It will be garbage collected when the plugin is unloaded + }, + + async onConfigChange(newConfig) { + console.log(LoggerPrefix, '[SlackNowPlaying] Config changed:', newConfig.enabled ? 'enabled' : 'disabled'); + }, +}); diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts new file mode 100644 index 0000000000..31682b7306 --- /dev/null +++ b/src/plugins/slack-now-playing/menu.ts @@ -0,0 +1,80 @@ +import prompt from 'custom-electron-prompt'; +import { BrowserWindow } from 'electron'; +import promptOptions from '@/providers/prompt-options'; + +import type { SlackNowPlayingConfig } from './main'; +import type { MenuContext } from '@/types/contexts'; +import type { MenuTemplate } from '@/menu'; + +/** + * Prompts user for Slack Now Playing plugin settings + */ +async function promptSlackNowPlayingOptions( + options: SlackNowPlayingConfig, + setConfig: (config: SlackNowPlayingConfig) => void, + window: BrowserWindow, +) { + const output = await prompt( + { + title: 'Slack Now Playing Settings', + label: 'Slack Now Playing Settings', + type: 'multiInput', + multiInputOptions: [ + { + label: 'Slack OAuth Token', + value: options.token, + inputAttrs: { + type: 'text', + }, + }, + { + label: 'Slack Cookie Token (d cookie value)', + value: options.cookieToken, + inputAttrs: { + type: 'text', + }, + }, + { + label: 'Emoji Name (for album art upload)', + value: options.emojiName, + inputAttrs: { + type: 'text', + }, + }, + ], + resizable: true, + height: 360, + ...promptOptions(), + }, + window, + ); + + if (output) { + if (output[0]) { + options.token = output[0]; + } + if (output[1]) { + options.cookieToken = output[1]; + } + if (output[2]) { + options.emojiName = output[2]; + } + setConfig(options); + } +} + +export const onMenu = async ({ + window, + getConfig, + setConfig, +}: MenuContext): Promise => { + const config = await getConfig(); + return [ + { + label: 'Settings', + click() { + promptSlackNowPlayingOptions(config, setConfig, window); + }, + }, + ]; +}; diff --git a/src/plugins/scrobbler/services/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts similarity index 100% rename from src/plugins/scrobbler/services/slack-api-client.ts rename to src/plugins/slack-now-playing/slack-api-client.ts From b7f7e732ce0eb39e18a51cffa17446fc5b6656d8 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 10:08:26 -0600 Subject: [PATCH 03/29] (fix) Remove unneeded imports and debug logging --- src/plugins/slack-now-playing/index.ts | 12 ---- src/plugins/slack-now-playing/main.ts | 90 +++++--------------------- 2 files changed, 17 insertions(+), 85 deletions(-) diff --git a/src/plugins/slack-now-playing/index.ts b/src/plugins/slack-now-playing/index.ts index 7c80523934..eed959d310 100644 --- a/src/plugins/slack-now-playing/index.ts +++ b/src/plugins/slack-now-playing/index.ts @@ -1,15 +1,3 @@ -import { createPlugin } from '@/utils'; -import { onMenu } from './menu'; -import type { SlackNowPlayingConfig, SongInfo } from './main'; -import { SlackNowPlaying } from './main'; - -export const defaultConfig: SlackNowPlayingConfig = { - enabled: false, - token: '', - cookieToken: '', - emojiName: 'my-album-art' -}; - import { createPlugin } from '@/utils'; import { onMenu } from './menu'; import { backend, SlackNowPlayingConfig } from './main'; diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index baf0a8937b..c100122901 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import os from 'node:os'; import { SlackApiClient, SlackApiResponse } from './slack-api-client'; import FormData from 'form-data'; -import { createBackend, LoggerPrefix } from '@/utils'; +import { createBackend } from '@/utils'; import registerCallback, { SongInfoEvent } from '@/providers/song-info'; // Import SongInfo type from provider instead of defining our own @@ -19,26 +19,6 @@ export interface SlackNowPlayingConfig { alternativeTitles?: boolean; } -interface SlackProfileData { - status_text: string; - status_emoji: string; - status_expiration?: number; -} - -interface SlackProfileUpdateData { - token: string; - profile: string; // JSON stringified profile data -} - - - -export interface SlackNowPlayingConfig { - enabled: boolean; - token: string; - cookieToken: string; - emojiName: string; -} - const defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; const state = { @@ -48,23 +28,17 @@ const state = { }; function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackNowPlayingConfig { - console.log(LoggerPrefix, '[SlackNowPlaying] Validating config'); if (!config.token || !config.cookieToken || !config.emojiName) { - console.error(LoggerPrefix, '[SlackNowPlaying] Missing required config values'); throw new Error('Missing Slack config values'); } - console.log(LoggerPrefix, '[SlackNowPlaying] Config validation successful'); } async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { - console.log(LoggerPrefix, '[SlackNowPlaying] Setting now playing status for:', songInfo.title); - try { validateConfig(config); // Skip if song is paused if (songInfo.isPaused) { - console.log(LoggerPrefix, '[SlackNowPlaying] Song is paused, not updating status'); return; } @@ -74,18 +48,14 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) let statusText = `Now Playing: ${truncatedArtist} - ${title}`; if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; - console.log(LoggerPrefix, `[SlackNowPlaying] Status text: "${statusText}"`); - // Calculate expiration time (current time + remaining song duration) const elapsed = songInfo.elapsedSeconds ?? 0; const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); const expirationTime = Math.floor(Date.now() / 1000) + remaining; - console.log(LoggerPrefix, `[SlackNowPlaying] Expiration time: ${new Date(expirationTime * 1000).toLocaleTimeString()}`); - await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); } catch (error) { - console.error(LoggerPrefix, '[SlackNowPlaying] Error setting now playing status:', error); + console.error(`Error setting Slack status: ${error}`); } } @@ -95,15 +65,11 @@ async function updateSlackStatusWithEmoji( songInfo: SongInfo, config: SlackNowPlayingConfig, ) { - console.log(LoggerPrefix, '[SlackNowPlaying] Updating Slack status with emoji'); - try { validateConfig(config); const client = new SlackApiClient(config.token, config.cookieToken); - console.log(LoggerPrefix, '[SlackNowPlaying] Getting status emoji'); const statusEmoji = await getStatusEmoji(songInfo, config); - console.log(LoggerPrefix, `[SlackNowPlaying] Using emoji: ${statusEmoji}`); const profileData = { status_text: statusText, @@ -116,30 +82,27 @@ async function updateSlackStatusWithEmoji( profile: JSON.stringify(profileData), }; - console.log(LoggerPrefix, '[SlackNowPlaying] Sending request to Slack API'); const res = await client.post('users.profile.set', postData); const json = res.data as SlackApiResponse; if (!json.ok) { - console.error(LoggerPrefix, `[SlackNowPlaying] Slack API error: ${json.error}`); + console.error(`Slack API error: ${json.error}`); } else { - console.log(LoggerPrefix, '[SlackNowPlaying] Successfully updated Slack status'); state.lastStatus = statusText; state.lastEmoji = statusEmoji; } } catch (error) { - console.error(LoggerPrefix, '[SlackNowPlaying] Error updating Slack status:', error); + console.error(`Error updating Slack status: ${error}`); } } async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { - if (songInfo.imageSrc) { - const emojiUploaded = await uploadEmojiToSlack(songInfo, config); - if (emojiUploaded) { - return `:${config.emojiName}:`; - } + if (songInfo.imageSrc && await uploadEmojiToSlack(songInfo, config)) { + return `:${config.emojiName}:`; } - return defaultEmojis[Math.floor(Math.random() * defaultEmojis.length)]; + + const randomIndex = Math.floor(Math.random() * defaultEmojis.length); + return defaultEmojis[randomIndex]; } async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { @@ -161,7 +124,7 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon const res = await client.post('emoji.add', formData, true); const json = res.data as SlackApiResponse; if (json.ok) return true; - console.error(`[SlackNowPlaying] Error uploading emoji: ${json.error}`); + console.error(`Error uploading emoji: ${json.error}`); return false; } @@ -175,7 +138,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { fs.writeFileSync(filePath, imageBuffer); return filePath; } catch (error) { - console.error('[SlackNowPlaying] Error saving album art to file:', error); + console.error(`Error saving album art to file: ${error}`); return null; } } @@ -192,7 +155,7 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { // Skip time change events if (event === SongInfoEvent.TimeChanged) return; - console.log(LoggerPrefix, `[SlackNowPlaying] Song info event: ${event}`); - // Only update if plugin is enabled if (!config.enabled) { - console.log(LoggerPrefix, '[SlackNowPlaying] Plugin is disabled, not updating status'); return; } @@ -235,43 +193,29 @@ export const backend = createBackend({ // Check if config has the expected structure const slackConfig = config as any; if (!slackConfig || typeof slackConfig !== 'object') { - console.error(LoggerPrefix, '[SlackNowPlaying] Invalid config object'); return; } - // Log the config for debugging - console.log(LoggerPrefix, '[SlackNowPlaying] Config:', JSON.stringify({ - enabled: slackConfig.enabled, - hasToken: Boolean(slackConfig.token), - hasCookieToken: Boolean(slackConfig.cookieToken), - hasEmojiName: Boolean(slackConfig.emojiName) - })); - // Make sure the config has the required properties if (!slackConfig.token || !slackConfig.cookieToken || !slackConfig.emojiName) { - console.error(LoggerPrefix, '[SlackNowPlaying] Missing required config values'); return; } - console.log(LoggerPrefix, '[SlackNowPlaying] Updating status with song:', songInfo.title); setNowPlaying(songInfo, slackConfig) - .catch(error => console.error(LoggerPrefix, '[SlackNowPlaying] Error in callback:', error)); + .catch(error => console.error(`Error in Slack Now Playing: ${error}`)); } catch (error) { - console.error(LoggerPrefix, '[SlackNowPlaying] Error processing song info:', error); + console.error(`Error processing song info: ${error}`); } }); - - console.log(LoggerPrefix, '[SlackNowPlaying] Registered song info callback'); }, async stop() { - console.log(LoggerPrefix, '[SlackNowPlaying] Stopping plugin'); state.window = undefined; // Note: We don't unregister the callback as there's no API for that // It will be garbage collected when the plugin is unloaded }, - async onConfigChange(newConfig) { - console.log(LoggerPrefix, '[SlackNowPlaying] Config changed:', newConfig.enabled ? 'enabled' : 'disabled'); + async onConfigChange() { + // Config changes will be picked up on the next song change }, }); From bbb668563c167d75f75a9862f05e3a41280fa63a Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 10:17:17 -0600 Subject: [PATCH 04/29] Add i18n for Slack Now Playing plugin --- src/i18n/resources/de.json | 10 ++++++++++ src/i18n/resources/en.json | 10 ++++++++++ src/i18n/resources/es.json | 10 ++++++++++ src/i18n/resources/fr.json | 10 ++++++++++ src/i18n/resources/ja.json | 10 ++++++++++ src/i18n/resources/pt-BR.json | 10 ++++++++++ src/plugins/slack-now-playing/index.ts | 5 +++-- src/plugins/slack-now-playing/menu.ts | 13 +++++++------ 8 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/i18n/resources/de.json b/src/i18n/resources/de.json index 64e77e2b2d..7882d0b50e 100644 --- a/src/i18n/resources/de.json +++ b/src/i18n/resources/de.json @@ -392,6 +392,16 @@ }, "name": "Deaktiviere automatisches Abspielen" }, + "slack-now-playing": { + "description": "Setzt deinen Slack-Status auf den aktuell abgespielten Song", + "menu": { + "settings": "Einstellungen", + "token": "Slack API-Token", + "cookie-token": "Slack Cookie-Token", + "emoji-name": "Benutzerdefinierter Emoji-Name" + }, + "name": "Slack Now Playing" + }, "discord": { "backend": { "already-connected": "Verbindungsaufbau bei aktiver Verbindung versucht", diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 824a681e51..3f05204de4 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -418,6 +418,16 @@ } } }, + "slack-now-playing": { + "description": "Sets your Slack status to the currently playing song", + "menu": { + "settings": "Settings", + "token": "Slack API Token", + "cookie-token": "Slack Cookie Token", + "emoji-name": "Custom Emoji Name" + }, + "name": "Slack Now Playing" + }, "downloader": { "backend": { "dialog": { diff --git a/src/i18n/resources/es.json b/src/i18n/resources/es.json index 22bc9adc6e..47c01d4841 100644 --- a/src/i18n/resources/es.json +++ b/src/i18n/resources/es.json @@ -392,6 +392,16 @@ }, "name": "Desactivar reproducción automática" }, + "slack-now-playing": { + "description": "Establece tu estado de Slack con la canción que estás reproduciendo", + "menu": { + "settings": "Configuración", + "token": "Token de API de Slack", + "cookie-token": "Token de Cookie de Slack", + "emoji-name": "Nombre del emoji personalizado" + }, + "name": "Slack Now Playing" + }, "discord": { "backend": { "already-connected": "Se intentó conectar con una conexión activa", diff --git a/src/i18n/resources/fr.json b/src/i18n/resources/fr.json index 5cf3cf2060..d4df69d632 100644 --- a/src/i18n/resources/fr.json +++ b/src/i18n/resources/fr.json @@ -392,6 +392,16 @@ }, "name": "Désactiver la lecture automatique" }, + "slack-now-playing": { + "description": "Définit votre statut Slack sur la chanson en cours de lecture", + "menu": { + "settings": "Paramètres", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Nom de l'emoji personnalisé" + }, + "name": "Slack Now Playing" + }, "discord": { "backend": { "already-connected": "Tentative de connexion avec une connexion active", diff --git a/src/i18n/resources/ja.json b/src/i18n/resources/ja.json index 29e552370d..a06564b7be 100644 --- a/src/i18n/resources/ja.json +++ b/src/i18n/resources/ja.json @@ -392,6 +392,16 @@ }, "name": "自動再生を無効化" }, + "slack-now-playing": { + "description": "現在再生中の曲をSlackステータスに設定します", + "menu": { + "settings": "設定", + "token": "Slack APIトークン", + "cookie-token": "Slack Cookieトークン", + "emoji-name": "カスタム絵文字名" + }, + "name": "Slack Now Playing" + }, "discord": { "backend": { "already-connected": "すでに有効になっている接続に接続を試みました", diff --git a/src/i18n/resources/pt-BR.json b/src/i18n/resources/pt-BR.json index a114cfea86..afa478aa0f 100644 --- a/src/i18n/resources/pt-BR.json +++ b/src/i18n/resources/pt-BR.json @@ -392,6 +392,16 @@ }, "name": "Desativar reprodução automática" }, + "slack-now-playing": { + "description": "Define seu status do Slack para a música que está tocando atualmente", + "menu": { + "settings": "Configurações", + "token": "Token de API do Slack", + "cookie-token": "Token de Cookie do Slack", + "emoji-name": "Nome do emoji personalizado" + }, + "name": "Slack Now Playing" + }, "discord": { "backend": { "already-connected": "Tentativa de conectar-se com conexão ativa", diff --git a/src/plugins/slack-now-playing/index.ts b/src/plugins/slack-now-playing/index.ts index eed959d310..bfeb4a4047 100644 --- a/src/plugins/slack-now-playing/index.ts +++ b/src/plugins/slack-now-playing/index.ts @@ -1,10 +1,11 @@ import { createPlugin } from '@/utils'; import { onMenu } from './menu'; import { backend, SlackNowPlayingConfig } from './main'; +import { t } from '@/i18n'; export default createPlugin({ - name: () => 'Slack Now Playing', - description: () => 'Sets your Slack status to the currently playing song.', + name: () => t('plugins.slack-now-playing.name'), + description: () => t('plugins.slack-now-playing.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts index 31682b7306..ddc18cdf55 100644 --- a/src/plugins/slack-now-playing/menu.ts +++ b/src/plugins/slack-now-playing/menu.ts @@ -1,6 +1,7 @@ import prompt from 'custom-electron-prompt'; import { BrowserWindow } from 'electron'; import promptOptions from '@/providers/prompt-options'; +import { t } from '@/i18n'; import type { SlackNowPlayingConfig } from './main'; import type { MenuContext } from '@/types/contexts'; @@ -16,26 +17,26 @@ async function promptSlackNowPlayingOptions( ) { const output = await prompt( { - title: 'Slack Now Playing Settings', - label: 'Slack Now Playing Settings', + title: t('plugins.slack-now-playing.name'), + label: t('plugins.slack-now-playing.name'), type: 'multiInput', multiInputOptions: [ { - label: 'Slack OAuth Token', + label: t('plugins.slack-now-playing.menu.token'), value: options.token, inputAttrs: { type: 'text', }, }, { - label: 'Slack Cookie Token (d cookie value)', + label: t('plugins.slack-now-playing.menu.cookie-token'), value: options.cookieToken, inputAttrs: { type: 'text', }, }, { - label: 'Emoji Name (for album art upload)', + label: t('plugins.slack-now-playing.menu.emoji-name'), value: options.emojiName, inputAttrs: { type: 'text', @@ -71,7 +72,7 @@ export const onMenu = async ({ const config = await getConfig(); return [ { - label: 'Settings', + label: t('plugins.slack-now-playing.menu.settings'), click() { promptSlackNowPlayingOptions(config, setConfig, window); }, From 7a5de99f4e3e581910482c106210a8332027d126 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 10:25:53 -0600 Subject: [PATCH 05/29] (i18n) Update plugin name and status text to use different languages --- src/i18n/resources/de.json | 3 ++- src/i18n/resources/en.json | 3 ++- src/i18n/resources/es.json | 3 ++- src/i18n/resources/fr.json | 3 ++- src/i18n/resources/ja.json | 3 ++- src/i18n/resources/pt-BR.json | 3 ++- src/plugins/slack-now-playing/main.ts | 9 ++++++++- 7 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/i18n/resources/de.json b/src/i18n/resources/de.json index 7882d0b50e..d4e56242cd 100644 --- a/src/i18n/resources/de.json +++ b/src/i18n/resources/de.json @@ -400,7 +400,8 @@ "cookie-token": "Slack Cookie-Token", "emoji-name": "Benutzerdefinierter Emoji-Name" }, - "name": "Slack Now Playing" + "name": "Slack Status", + "status-text": "Jetzt spielt: {{artist}} - {{title}}" }, "discord": { "backend": { diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 3f05204de4..58b83ed5ad 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -426,7 +426,8 @@ "cookie-token": "Slack Cookie Token", "emoji-name": "Custom Emoji Name" }, - "name": "Slack Now Playing" + "name": "Slack Status", + "status-text": "Now Playing: {{artist}} - {{title}}" }, "downloader": { "backend": { diff --git a/src/i18n/resources/es.json b/src/i18n/resources/es.json index 47c01d4841..bf8607f12e 100644 --- a/src/i18n/resources/es.json +++ b/src/i18n/resources/es.json @@ -400,7 +400,8 @@ "cookie-token": "Token de Cookie de Slack", "emoji-name": "Nombre del emoji personalizado" }, - "name": "Slack Now Playing" + "name": "Estado de Slack", + "status-text": "Reproduciendo: {{artist}} - {{title}}" }, "discord": { "backend": { diff --git a/src/i18n/resources/fr.json b/src/i18n/resources/fr.json index d4df69d632..90b06ab8f2 100644 --- a/src/i18n/resources/fr.json +++ b/src/i18n/resources/fr.json @@ -400,7 +400,8 @@ "cookie-token": "Token Cookie Slack", "emoji-name": "Nom de l'emoji personnalisé" }, - "name": "Slack Now Playing" + "name": "Statut Slack", + "status-text": "En écoute : {{artist}} - {{title}}" }, "discord": { "backend": { diff --git a/src/i18n/resources/ja.json b/src/i18n/resources/ja.json index a06564b7be..fa9ecc86c9 100644 --- a/src/i18n/resources/ja.json +++ b/src/i18n/resources/ja.json @@ -400,7 +400,8 @@ "cookie-token": "Slack Cookieトークン", "emoji-name": "カスタム絵文字名" }, - "name": "Slack Now Playing" + "name": "Slackステータス", + "status-text": "再生中: {{artist}} - {{title}}" }, "discord": { "backend": { diff --git a/src/i18n/resources/pt-BR.json b/src/i18n/resources/pt-BR.json index afa478aa0f..e631145928 100644 --- a/src/i18n/resources/pt-BR.json +++ b/src/i18n/resources/pt-BR.json @@ -400,7 +400,8 @@ "cookie-token": "Token de Cookie do Slack", "emoji-name": "Nome do emoji personalizado" }, - "name": "Slack Now Playing" + "name": "Status do Slack", + "status-text": "Tocando agora: {{artist}} - {{title}}" }, "discord": { "backend": { diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index c100122901..1e4149be41 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -6,6 +6,7 @@ import { SlackApiClient, SlackApiResponse } from './slack-api-client'; import FormData from 'form-data'; import { createBackend } from '@/utils'; import registerCallback, { SongInfoEvent } from '@/providers/song-info'; +import { t } from '@/i18n'; // Import SongInfo type from provider instead of defining our own import type { SongInfo } from '@/providers/song-info'; @@ -45,7 +46,13 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) const title = songInfo.alternativeTitle ?? songInfo.title; const artistPart = songInfo.artist || 'Unknown Artist'; const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; - let statusText = `Now Playing: ${truncatedArtist} - ${title}`; + + // Use localized version of the status text + let statusText = t('plugins.slack-now-playing.status-text') + .replace('{{artist}}', truncatedArtist) + .replace('{{title}}', title); + + // Ensure the status text doesn't exceed Slack's limit if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; // Calculate expiration time (current time + remaining song duration) From caf25a3910974cdb5688a63d75d13421e3de2c67 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 11:02:16 -0600 Subject: [PATCH 06/29] (i18n) Add localizations for Slack Now Playing --- src/i18n/resources/ar.json | 13 ++++++++++++- src/i18n/resources/bg.json | 11 +++++++++++ src/i18n/resources/bs.json | 15 ++++++++++++++- src/i18n/resources/ca.json | 13 ++++++++++++- src/i18n/resources/cs.json | 13 ++++++++++++- src/i18n/resources/da.json | 13 ++++++++++++- src/i18n/resources/el.json | 13 ++++++++++++- src/i18n/resources/et.json | 13 ++++++++++++- src/i18n/resources/fa.json | 13 ++++++++++++- src/i18n/resources/fi.json | 13 ++++++++++++- src/i18n/resources/fil.json | 13 ++++++++++++- src/i18n/resources/he.json | 13 ++++++++++++- src/i18n/resources/hi.json | 13 ++++++++++++- src/i18n/resources/hr.json | 15 ++++++++++++++- src/i18n/resources/hu.json | 13 ++++++++++++- src/i18n/resources/id.json | 13 ++++++++++++- src/i18n/resources/is.json | 13 ++++++++++++- src/i18n/resources/it.json | 13 ++++++++++++- src/i18n/resources/ka.json | 13 ++++++++++++- src/i18n/resources/kn.json | 16 +++++++++++++++- src/i18n/resources/ko.json | 13 ++++++++++++- src/i18n/resources/lt.json | 13 ++++++++++++- src/i18n/resources/ml.json | 15 ++++++++++++++- src/i18n/resources/ms.json | 13 ++++++++++++- src/i18n/resources/nb.json | 13 ++++++++++++- src/i18n/resources/ne.json | 13 ++++++++++++- src/i18n/resources/nl.json | 13 ++++++++++++- src/i18n/resources/pl.json | 13 ++++++++++++- src/i18n/resources/pt.json | 13 ++++++++++++- src/i18n/resources/ro.json | 13 ++++++++++++- src/i18n/resources/ru.json | 13 ++++++++++++- src/i18n/resources/si.json | 15 ++++++++++++++- src/i18n/resources/sl.json | 13 ++++++++++++- src/i18n/resources/sr.json | 16 +++++++++++++++- src/i18n/resources/sv.json | 13 ++++++++++++- src/i18n/resources/ta.json | 13 ++++++++++++- src/i18n/resources/th.json | 13 ++++++++++++- src/i18n/resources/tr.json | 13 ++++++++++++- src/i18n/resources/uk.json | 13 ++++++++++++- src/i18n/resources/ur.json | 15 ++++++++++++++- src/i18n/resources/vi.json | 13 ++++++++++++- src/i18n/resources/zh-CN.json | 13 ++++++++++++- src/i18n/resources/zh-TW.json | 13 ++++++++++++- 43 files changed, 531 insertions(+), 42 deletions(-) diff --git a/src/i18n/resources/ar.json b/src/i18n/resources/ar.json index be3714bbbf..1caa81642f 100644 --- a/src/i18n/resources/ar.json +++ b/src/i18n/resources/ar.json @@ -840,6 +840,17 @@ "visualizer-type": "نوع المعاينة المصرية" }, "name": "معاين بصري" + }, + "slack-now-playing": { + "name": "حالة سلاك", + "description": "يضبط حالة سلاك إلى الأغنية التي تعمل حاليًا", + "status-text": "يشغل الآن: {{artist}} - {{title}}", + "menu": { + "settings": "الإعدادات", + "token": "رمز API سلاك", + "cookie-token": "رمز كوكي سلاك", + "emoji-name": "اسم الرمز التعبيري المخصص" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/bg.json b/src/i18n/resources/bg.json index 014dedb734..5c5198960c 100644 --- a/src/i18n/resources/bg.json +++ b/src/i18n/resources/bg.json @@ -652,6 +652,17 @@ } }, "description": "Позволява промяна на качеството на видеото с бутон върху видеото" + }, + "slack-now-playing": { + "name": "Статус в Slack", + "description": "Задава вашия статус в Slack на текущо възпроизвежданата песен", + "status-text": "Сега свири: {{artist}} - {{title}}", + "menu": { + "settings": "Настройки", + "token": "Slack API Токен", + "cookie-token": "Slack Cookie Токен", + "emoji-name": "Име на персонализиран emoji" + } } } } diff --git a/src/i18n/resources/bs.json b/src/i18n/resources/bs.json index fd874c2bf7..73eef89fb3 100644 --- a/src/i18n/resources/bs.json +++ b/src/i18n/resources/bs.json @@ -55,5 +55,18 @@ "detail": "\"{{pluginName}}\" dodatak zahtjeva ponovno pokretanje kako bi se uključio" } } + }, + "plugins": { + "slack-now-playing": { + "name": "Slack status", + "description": "Postavlja vaš Slack status na pjesmu koja se trenutno reproducira", + "status-text": "Sada svira: {{artist}} - {{title}}", + "menu": { + "settings": "Postavke", + "token": "Slack API token", + "cookie-token": "Slack Cookie token", + "emoji-name": "Naziv prilagođenog emoji-ja" + } + } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ca.json b/src/i18n/resources/ca.json index 90504e741a..acc91310f1 100644 --- a/src/i18n/resources/ca.json +++ b/src/i18n/resources/ca.json @@ -808,6 +808,17 @@ "visualizer-type": "Tipus de visualitzador" }, "name": "Visualitzador" + }, + "slack-now-playing": { + "name": "Slack Status", + "description": "Sets your Slack status to the currently playing song", + "status-text": "Now Playing: {{artist}} - {{title}}", + "menu": { + "settings": "Settings", + "token": "Slack API Token", + "cookie-token": "Slack Cookie Token", + "emoji-name": "Custom Emoji Name" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/cs.json b/src/i18n/resources/cs.json index b2786e678a..3a8b8019fb 100644 --- a/src/i18n/resources/cs.json +++ b/src/i18n/resources/cs.json @@ -774,6 +774,17 @@ "visualizer-type": "Typ vizualizéru" }, "name": "Vizualizér" + }, + "slack-now-playing": { + "name": "Slack status", + "description": "Nastaví váš Slack status na aktuálně přehrávanou píseň", + "status-text": "Nyní hraje: {{artist}} - {{title}}", + "menu": { + "settings": "Nastavení", + "token": "Slack API token", + "cookie-token": "Slack Cookie token", + "emoji-name": "Název vlastního emoji" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/da.json b/src/i18n/resources/da.json index 729e96493f..d5afc95969 100644 --- a/src/i18n/resources/da.json +++ b/src/i18n/resources/da.json @@ -310,6 +310,17 @@ "advanced": "Avanceret" }, "name": "Fade [Beta]" + }, + "slack-now-playing": { + "name": "Slack-status", + "description": "Sætter din Slack-status til den sang, der afspilles i øjeblikket", + "status-text": "Spiller nu: {{artist}} - {{title}}", + "menu": { + "settings": "Indstillinger", + "token": "Slack API-token", + "cookie-token": "Slack Cookie-token", + "emoji-name": "Brugerdefineret emoji-navn" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/el.json b/src/i18n/resources/el.json index ffce99d261..fa5978c03c 100644 --- a/src/i18n/resources/el.json +++ b/src/i18n/resources/el.json @@ -593,6 +593,17 @@ "templates": { "button": "Τραγούδι" } + }, + "slack-now-playing": { + "name": "Κατάσταση Slack", + "description": "Ορίζει την κατάσταση του Slack στο τραγούδι που παίζει αυτή τη στιγμή", + "status-text": "Παίζει τώρα: {{artist}} - {{title}}", + "menu": { + "settings": "Ρυθμίσεις", + "token": "Διακριτικό API Slack", + "cookie-token": "Διακριτικό Cookie Slack", + "emoji-name": "Όνομα προσαρμοσμένου emoji" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/et.json b/src/i18n/resources/et.json index 9c471c2823..3e9502a108 100644 --- a/src/i18n/resources/et.json +++ b/src/i18n/resources/et.json @@ -219,6 +219,17 @@ }, "tuna-obs": { "description": "Lõimimine OBSi Tuna lisamooduliga" + }, + "slack-now-playing": { + "name": "Slacki olek", + "description": "Seadistab sinu Slacki oleku hetkel mängiva laulu järgi", + "status-text": "Praegu mängib: {{artist}} - {{title}}", + "menu": { + "settings": "Seaded", + "token": "Slacki API võti", + "cookie-token": "Slacki küpsise võti", + "emoji-name": "Kohandatud emoji nimi" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/fa.json b/src/i18n/resources/fa.json index b7253df413..f3771d6fd7 100644 --- a/src/i18n/resources/fa.json +++ b/src/i18n/resources/fa.json @@ -840,6 +840,17 @@ "visualizer-type": "نوع نمایش‌دهنده تصویری" }, "name": "نمایش‌دهنده تصویری" + }, + "slack-now-playing": { + "name": "وضعیت Slack", + "description": "وضعیت Slack شما را به آهنگ در حال پخش تنظیم می‌کند", + "status-text": "در حال پخش: {{artist}} - {{title}}", + "menu": { + "settings": "تنظیمات", + "token": "توکن API Slack", + "cookie-token": "توکن کوکی Slack", + "emoji-name": "نام ایموجی سفارشی" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/fi.json b/src/i18n/resources/fi.json index adbecff330..e2d35b8412 100644 --- a/src/i18n/resources/fi.json +++ b/src/i18n/resources/fi.json @@ -648,6 +648,17 @@ } } } + }, + "slack-now-playing": { + "name": "Slack-tila", + "description": "Asettaa Slack-tilasi nykyisin soivaan kappaleeseen", + "status-text": "Nyt soi: {{artist}} - {{title}}", + "menu": { + "settings": "Asetukset", + "token": "Slack API -tunnus", + "cookie-token": "Slack Cookie -tunnus", + "emoji-name": "Mukautetun emojin nimi" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/fil.json b/src/i18n/resources/fil.json index 6580b45e58..4e1e96e200 100644 --- a/src/i18n/resources/fil.json +++ b/src/i18n/resources/fil.json @@ -788,6 +788,17 @@ "menu": { "visualizer-type": "Uri ng Visualizer" } + }, + "slack-now-playing": { + "name": "Slack Status", + "description": "Sets your Slack status to the currently playing song", + "status-text": "Now Playing: {{artist}} - {{title}}", + "menu": { + "settings": "Settings", + "token": "Slack API Token", + "cookie-token": "Slack Cookie Token", + "emoji-name": "Custom Emoji Name" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/he.json b/src/i18n/resources/he.json index e3c851d8d1..37910dd7d2 100644 --- a/src/i18n/resources/he.json +++ b/src/i18n/resources/he.json @@ -206,6 +206,17 @@ "ad-speedup": { "description": "במקרה של פרסומת, הסאונד מושתק ומהירות הוידרו מוכפלת ב-16", "name": "הגבר מהירות פרסומת" + }, + "slack-now-playing": { + "name": "סטטוס Slack", + "description": "מגדיר את סטטוס ה-Slack שלך לשיר המושמע כעת", + "status-text": "מנגן כעת: {{artist}} - {{title}}", + "menu": { + "settings": "הגדרות", + "token": "אסימון API של Slack", + "cookie-token": "אסימון עוגייה של Slack", + "emoji-name": "שם אימוג'י מותאם אישית" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/hi.json b/src/i18n/resources/hi.json index 472ad7b8e8..da94bfdb73 100644 --- a/src/i18n/resources/hi.json +++ b/src/i18n/resources/hi.json @@ -310,6 +310,17 @@ "label": "तरीका" } } + }, + "slack-now-playing": { + "name": "Slack स्थिति", + "description": "आपकी Slack स्थिति को वर्तमान में चल रहे गाने पर सेट करता है", + "status-text": "अभी बज रहा है: {{artist}} - {{title}}", + "menu": { + "settings": "सेटिंग्स", + "token": "Slack API टोकन", + "cookie-token": "Slack कुकी टोकन", + "emoji-name": "कस्टम इमोजी नाम" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/hr.json b/src/i18n/resources/hr.json index abddd4d11c..24e5599010 100644 --- a/src/i18n/resources/hr.json +++ b/src/i18n/resources/hr.json @@ -15,5 +15,18 @@ "code": "hr", "local-name": "Hrvatski", "name": "Croatian" + }, + "plugins": { + "slack-now-playing": { + "name": "Slack status", + "description": "Postavlja vaš Slack status na pjesmu koja se trenutno reproducira", + "status-text": "Sada svira: {{artist}} - {{title}}", + "menu": { + "settings": "Postavke", + "token": "Slack API token", + "cookie-token": "Slack Cookie token", + "emoji-name": "Naziv prilagođenog emojija" + } + } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/hu.json b/src/i18n/resources/hu.json index 4450b43864..5ec00cb31c 100644 --- a/src/i18n/resources/hu.json +++ b/src/i18n/resources/hu.json @@ -840,6 +840,17 @@ "visualizer-type": "Vizualizáció típus" }, "name": "Vizualizáció" + }, + "slack-now-playing": { + "name": "Slack állapot", + "description": "Beállítja a Slack állapotod az éppen játszó dalra", + "status-text": "Most játszik: {{artist}} - {{title}}", + "menu": { + "settings": "Beállítások", + "token": "Slack API token", + "cookie-token": "Slack Cookie token", + "emoji-name": "Egyéni emoji név" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/id.json b/src/i18n/resources/id.json index ac6548fe2b..a46d9abb60 100644 --- a/src/i18n/resources/id.json +++ b/src/i18n/resources/id.json @@ -840,6 +840,17 @@ "visualizer-type": "Tipe Visualisator" }, "name": "Visualisator" + }, + "slack-now-playing": { + "name": "Status Slack", + "description": "Mengatur status Slack Anda ke lagu yang sedang diputar", + "status-text": "Sedang Diputar: {{artist}} - {{title}}", + "menu": { + "settings": "Pengaturan", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Nama Emoji Kustom" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/is.json b/src/i18n/resources/is.json index f58bc147b2..4a66aa1e91 100644 --- a/src/i18n/resources/is.json +++ b/src/i18n/resources/is.json @@ -808,6 +808,17 @@ "visualizer-type": "Sýndarstýringartegund" }, "name": "Sýndarstýringar" + }, + "slack-now-playing": { + "name": "Slack staða", + "description": "Stillir Slack stöðuna þína á lagið sem er í spilun núna", + "status-text": "Spilar núna: {{artist}} - {{title}}", + "menu": { + "settings": "Stillingar", + "token": "Slack API lykill", + "cookie-token": "Slack köku lykill", + "emoji-name": "Sérsniðið táknmynd nafn" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/it.json b/src/i18n/resources/it.json index ddfd75486d..c25581fd2d 100644 --- a/src/i18n/resources/it.json +++ b/src/i18n/resources/it.json @@ -839,6 +839,17 @@ "visualizer-type": "Tipo di visualizzazione" }, "name": "Visualizzatore grafico" + }, + "slack-now-playing": { + "name": "Stato Slack", + "description": "Imposta il tuo stato Slack sulla canzone attualmente in riproduzione", + "status-text": "In riproduzione: {{artist}} - {{title}}", + "menu": { + "settings": "Impostazioni", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Nome emoji personalizzato" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ka.json b/src/i18n/resources/ka.json index 97f4d60e01..c97d0dd10c 100644 --- a/src/i18n/resources/ka.json +++ b/src/i18n/resources/ka.json @@ -281,6 +281,17 @@ } } } + }, + "slack-now-playing": { + "name": "Slack Status", + "description": "Sets your Slack status to the currently playing song", + "status-text": "Now Playing: {{artist}} - {{title}}", + "menu": { + "settings": "Settings", + "token": "Slack API Token", + "cookie-token": "Slack Cookie Token", + "emoji-name": "Custom Emoji Name" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/kn.json b/src/i18n/resources/kn.json index 0967ef424b..87900d15d0 100644 --- a/src/i18n/resources/kn.json +++ b/src/i18n/resources/kn.json @@ -1 +1,15 @@ -{} +{ + "plugins": { + "slack-now-playing": { + "name": "Slack ಸ್ಥಿತಿ", + "description": "ನಿಮ್ಮ Slack ಸ್ಥಿತಿಯನ್ನು ಪ್ರಸ್ತುತ ಪ್ಲೇ ಆಗುತ್ತಿರುವ ಹಾಡಿಗೆ ಹೊಂದಿಸುತ್ತದೆ", + "status-text": "ಈಗ ಪ್ಲೇ ಆಗುತ್ತಿದೆ: {{artist}} - {{title}}", + "menu": { + "settings": "ಸೆಟ್ಟಿಂಗ್ಸ್", + "token": "Slack API ಟೋಕನ್", + "cookie-token": "Slack ಕುಕೀ ಟೋಕನ್", + "emoji-name": "ಕಸ್ಟಮ್ ಎಮೋಜಿ ಹೆಸರು" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/resources/ko.json b/src/i18n/resources/ko.json index 9cd8593bb5..afeb31079f 100644 --- a/src/i18n/resources/ko.json +++ b/src/i18n/resources/ko.json @@ -840,6 +840,17 @@ "visualizer-type": "비주얼라이저 타입" }, "name": "비주얼라이저" + }, + "slack-now-playing": { + "name": "Slack 상태", + "description": "현재 재생 중인 노래로 Slack 상태를 설정합니다", + "status-text": "지금 재생 중: {{artist}} - {{title}}", + "menu": { + "settings": "설정", + "token": "Slack API 토큰", + "cookie-token": "Slack 쿠키 토큰", + "emoji-name": "사용자 정의 이모티콘 이름" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/lt.json b/src/i18n/resources/lt.json index b1c65c98b4..ae4b8e7f01 100644 --- a/src/i18n/resources/lt.json +++ b/src/i18n/resources/lt.json @@ -627,6 +627,17 @@ "visualizer-type": "Vizualizatoriaus tipas" }, "name": "Vizualizatorius" + }, + "slack-now-playing": { + "name": "Slack būsena", + "description": "Nustato jūsų Slack būseną pagal dabar grojamą dainą", + "status-text": "Dabar groja: {{artist}} - {{title}}", + "menu": { + "settings": "Nustatymai", + "token": "Slack API raktas", + "cookie-token": "Slack slapuko raktas", + "emoji-name": "Pasirinktinio emoji pavadinimas" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ml.json b/src/i18n/resources/ml.json index a2ce699e3a..fab46b3f5b 100644 --- a/src/i18n/resources/ml.json +++ b/src/i18n/resources/ml.json @@ -53,5 +53,18 @@ "message": "\"{{pluginName}}\" restart ആവശ്യപെടുന്നു" } } + }, + "plugins": { + "slack-now-playing": { + "name": "Slack സ്റ്റാറ്റസ്", + "description": "നിങ്ങളുടെ Slack സ്റ്റാറ്റസ് നിലവിൽ പ്ലേ ചെയ്യുന്ന ഗാനത്തിലേക്ക് സെറ്റ് ചെയ്യുന്നു", + "status-text": "ഇപ്പോൾ പ്ലേ ചെയ്യുന്നത്: {{artist}} - {{title}}", + "menu": { + "settings": "ക്രമീകരണങ്ങൾ", + "token": "Slack API ടോക്കൺ", + "cookie-token": "Slack കുക്കി ടോക്കൺ", + "emoji-name": "കസ്റ്റം ഇമോജി പേര്" + } + } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ms.json b/src/i18n/resources/ms.json index f2449ea644..2a1f2000b8 100644 --- a/src/i18n/resources/ms.json +++ b/src/i18n/resources/ms.json @@ -198,6 +198,17 @@ "templates": { "button": "Lagu" } + }, + "slack-now-playing": { + "name": "Status Slack", + "description": "Menetapkan status Slack anda kepada lagu yang sedang dimainkan", + "status-text": "Sedang Dimainkan: {{artist}} - {{title}}", + "menu": { + "settings": "Tetapan", + "token": "Token API Slack", + "cookie-token": "Token Kuki Slack", + "emoji-name": "Nama Emoji Tersuai" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/nb.json b/src/i18n/resources/nb.json index bae167834a..c1c897332e 100644 --- a/src/i18n/resources/nb.json +++ b/src/i18n/resources/nb.json @@ -586,6 +586,17 @@ "visualizer-type": "Visualisatortype" }, "name": "Visualisator" + }, + "slack-now-playing": { + "name": "Slack-status", + "description": "Setter Slack-statusen din til sangen som spilles for øyeblikket", + "status-text": "Spiller nå: {{artist}} - {{title}}", + "menu": { + "settings": "Innstillinger", + "token": "Slack API-token", + "cookie-token": "Slack Cookie-token", + "emoji-name": "Egendefinert emoji-navn" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ne.json b/src/i18n/resources/ne.json index 5d18e466e9..202399feb0 100644 --- a/src/i18n/resources/ne.json +++ b/src/i18n/resources/ne.json @@ -831,6 +831,17 @@ "visualizer-type": "भिजुअलाइजरको प्रकार" }, "name": "भिजुअलाइजर" + }, + "slack-now-playing": { + "name": "Slack स्टेटस", + "description": "तपाईंको Slack स्टेटसलाई हाल बजिरहेको गीतमा सेट गर्छ", + "status-text": "अहिले बज्दै: {{artist}} - {{title}}", + "menu": { + "settings": "सेटिङ्गहरु", + "token": "Slack API टोकन", + "cookie-token": "Slack कुकी टोकन", + "emoji-name": "कस्टम इमोजी नाम" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/nl.json b/src/i18n/resources/nl.json index 0838f6a8bc..b1dfb1e3d8 100644 --- a/src/i18n/resources/nl.json +++ b/src/i18n/resources/nl.json @@ -840,6 +840,17 @@ "visualizer-type": "Visualisatietype" }, "name": "Visualisator" + }, + "slack-now-playing": { + "name": "Slack Status", + "description": "Stelt je Slack-status in op het nummer dat momenteel wordt afgespeeld", + "status-text": "Nu aan het spelen: {{artist}} - {{title}}", + "menu": { + "settings": "Instellingen", + "token": "Slack API-token", + "cookie-token": "Slack Cookie-token", + "emoji-name": "Aangepaste emoji-naam" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/pl.json b/src/i18n/resources/pl.json index 6d26b47d52..40830b4a2d 100644 --- a/src/i18n/resources/pl.json +++ b/src/i18n/resources/pl.json @@ -840,6 +840,17 @@ "visualizer-type": "Typ wizualizatora" }, "name": "Wizualizator" + }, + "slack-now-playing": { + "name": "Status Slack", + "description": "Ustawia twój status Slack na aktualnie odtwarzaną piosenkę", + "status-text": "Teraz odtwarzane: {{artist}} - {{title}}", + "menu": { + "settings": "Ustawienia", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Nazwa niestandardowego emoji" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/pt.json b/src/i18n/resources/pt.json index d8886bab44..e67389fb48 100644 --- a/src/i18n/resources/pt.json +++ b/src/i18n/resources/pt.json @@ -840,6 +840,17 @@ "visualizer-type": "Tipo de visualizador" }, "name": "Visualizador" + }, + "slack-now-playing": { + "name": "Estado do Slack", + "description": "Define o seu estado do Slack para a música que está a tocar atualmente", + "status-text": "A tocar: {{artist}} - {{title}}", + "menu": { + "settings": "Configurações", + "token": "Token de API do Slack", + "cookie-token": "Token de Cookie do Slack", + "emoji-name": "Nome do emoji personalizado" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ro.json b/src/i18n/resources/ro.json index c5348ab139..90c247e1e5 100644 --- a/src/i18n/resources/ro.json +++ b/src/i18n/resources/ro.json @@ -830,6 +830,17 @@ "visualizer-type": "Tip de vizualizator" }, "name": "Vizualizator" + }, + "slack-now-playing": { + "name": "Status Slack", + "description": "Setează statusul tău de Slack la melodia care se redă în prezent", + "status-text": "În redare acum: {{artist}} - {{title}}", + "menu": { + "settings": "Setări", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Nume emoji personalizat" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ru.json b/src/i18n/resources/ru.json index 1eb5daf61c..95011c4031 100644 --- a/src/i18n/resources/ru.json +++ b/src/i18n/resources/ru.json @@ -840,6 +840,17 @@ "visualizer-type": "Вид визуализации" }, "name": "Визуализатор" + }, + "slack-now-playing": { + "name": "Статус Slack", + "description": "Устанавливает ваш статус Slack на текущую песню", + "status-text": "Сейчас играет: {{artist}} - {{title}}", + "menu": { + "settings": "Настройки", + "token": "Токен API Slack", + "cookie-token": "Токен Cookie Slack", + "emoji-name": "Имя пользовательского эмодзи" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/si.json b/src/i18n/resources/si.json index 687b7676ed..79345cd30b 100644 --- a/src/i18n/resources/si.json +++ b/src/i18n/resources/si.json @@ -104,5 +104,18 @@ } } } + }, + "plugins": { + "slack-now-playing": { + "name": "Slack තත්ත්වය", + "description": "ඔබේ Slack තත්ත්වය දැනට වාදනය වන ගීතයට සැකසේ", + "status-text": "දැන් වාදනය වන්නේ: {{artist}} - {{title}}", + "menu": { + "settings": "සැකසුම්", + "token": "Slack API ටෝකනය", + "cookie-token": "Slack කුකී ටෝකනය", + "emoji-name": "අභිමත ඉමෝජි නම" + } + } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/sl.json b/src/i18n/resources/sl.json index 57333dc17b..7afd05bc19 100644 --- a/src/i18n/resources/sl.json +++ b/src/i18n/resources/sl.json @@ -548,6 +548,17 @@ "id-copied": "Host ID je kopiran v odložišče", "id-copy-failed": "Host ID ni bilo mogoče kopirati" } + }, + "slack-now-playing": { + "name": "Slack status", + "description": "Nastavi vaš Slack status na trenutno predvajano pesem", + "status-text": "Zdaj predvaja: {{artist}} - {{title}}", + "menu": { + "settings": "Nastavitve", + "token": "Slack API žeton", + "cookie-token": "Slack Cookie žeton", + "emoji-name": "Ime emoji po meri" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/sr.json b/src/i18n/resources/sr.json index 0967ef424b..86fd1b984f 100644 --- a/src/i18n/resources/sr.json +++ b/src/i18n/resources/sr.json @@ -1 +1,15 @@ -{} +{ + "plugins": { + "slack-now-playing": { + "name": "Slack статус", + "description": "Поставља ваш Slack статус на песму која се тренутно репродукује", + "status-text": "Сада свира: {{artist}} - {{title}}", + "menu": { + "settings": "Подешавања", + "token": "Slack API токен", + "cookie-token": "Slack Cookie токен", + "emoji-name": "Назив прилагођеног емоджија" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/resources/sv.json b/src/i18n/resources/sv.json index b213fbdb52..7bd9fe3182 100644 --- a/src/i18n/resources/sv.json +++ b/src/i18n/resources/sv.json @@ -224,6 +224,17 @@ "templates": { "button": "Låt" } + }, + "slack-now-playing": { + "name": "Slack-status", + "description": "Ställer in din Slack-status till låten som spelas för närvarande", + "status-text": "Spelar nu: {{artist}} - {{title}}", + "menu": { + "settings": "Inställningar", + "token": "Slack API-token", + "cookie-token": "Slack Cookie-token", + "emoji-name": "Anpassat emoji-namn" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ta.json b/src/i18n/resources/ta.json index 17061a970a..47a9690979 100644 --- a/src/i18n/resources/ta.json +++ b/src/i18n/resources/ta.json @@ -831,6 +831,17 @@ "visualizer-type": "விசுவலைசர் வகை" }, "name": "காட்சிப்படுத்தல்" + }, + "slack-now-playing": { + "name": "Slack நிலை", + "description": "உங்கள் Slack நிலையை தற்போது இயங்கும் பாடலுக்கு அமைக்கிறது", + "status-text": "இப்போது இயங்குகிறது: {{artist}} - {{title}}", + "menu": { + "settings": "அமைப்புகள்", + "token": "Slack API டோக்கன்", + "cookie-token": "Slack குக்கீ டோக்கன்", + "emoji-name": "தனிப்பயன் எமோஜி பெயர்" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/th.json b/src/i18n/resources/th.json index b72f43d283..87f874fa0f 100644 --- a/src/i18n/resources/th.json +++ b/src/i18n/resources/th.json @@ -840,6 +840,17 @@ "visualizer-type": "ประเภทวิชวลไลเซอร์" }, "name": "วิชวลไลเซอร์" + }, + "slack-now-playing": { + "name": "สถานะ Slack", + "description": "ตั้งค่าสถานะ Slack ของคุณเป็นเพลงที่กำลังเล่นอยู่", + "status-text": "กำลังเล่น: {{artist}} - {{title}}", + "menu": { + "settings": "การตั้งค่า", + "token": "โทเค็น API Slack", + "cookie-token": "โทเค็นคุกกี้ Slack", + "emoji-name": "ชื่ออีโมจิที่กำหนดเอง" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/tr.json b/src/i18n/resources/tr.json index 5feaa67acc..c313464d54 100644 --- a/src/i18n/resources/tr.json +++ b/src/i18n/resources/tr.json @@ -840,6 +840,17 @@ "visualizer-type": "Görselleştirici Tipi" }, "name": "Görselleştirici" + }, + "slack-now-playing": { + "name": "Slack Durumu", + "description": "Slack durumunuzu şu anda çalan şarkıya ayarlar", + "status-text": "Şu anda çalıyor: {{artist}} - {{title}}", + "menu": { + "settings": "Ayarlar", + "token": "Slack API Jetonu", + "cookie-token": "Slack Çerez Jetonu", + "emoji-name": "Özel Emoji Adı" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/uk.json b/src/i18n/resources/uk.json index a851471da3..7348cc9204 100644 --- a/src/i18n/resources/uk.json +++ b/src/i18n/resources/uk.json @@ -840,6 +840,17 @@ "visualizer-type": "Тип візуалізації" }, "name": "Візуалізація" + }, + "slack-now-playing": { + "name": "Статус Slack", + "description": "Встановлює ваш статус Slack на пісню, яка зараз грає", + "status-text": "Зараз грає: {{artist}} - {{title}}", + "menu": { + "settings": "Налаштування", + "token": "Токен API Slack", + "cookie-token": "Токен Cookie Slack", + "emoji-name": "Назва користувацького емодзі" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/ur.json b/src/i18n/resources/ur.json index 2602c00a68..b37085c476 100644 --- a/src/i18n/resources/ur.json +++ b/src/i18n/resources/ur.json @@ -200,5 +200,18 @@ "quit": "باہر نکلیں", "restart": "ایپ دوبارہ شروع کریں" } + }, + "plugins": { + "slack-now-playing": { + "name": "Slack حیثیت", + "description": "آپ کی Slack حیثیت کو موجودہ چل رہے گانے پر سیٹ کرتا ہے", + "status-text": "اب چل رہا ہے: {{artist}} - {{title}}", + "menu": { + "settings": "ترتیبات", + "token": "Slack API ٹوکن", + "cookie-token": "Slack کوکی ٹوکن", + "emoji-name": "حسب ضرورت ایموجی نام" + } + } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/vi.json b/src/i18n/resources/vi.json index 4b5996e46a..efd0d165b3 100644 --- a/src/i18n/resources/vi.json +++ b/src/i18n/resources/vi.json @@ -824,6 +824,17 @@ "visualizer-type": "Loại trình hiển thị" }, "name": "Trình hiển thị" + }, + "slack-now-playing": { + "name": "Trạng thái Slack", + "description": "Đặt trạng thái Slack của bạn thành bài hát đang phát hiện tại", + "status-text": "Đang phát: {{artist}} - {{title}}", + "menu": { + "settings": "Cài đặt", + "token": "Token API Slack", + "cookie-token": "Token Cookie Slack", + "emoji-name": "Tên emoji tùy chỉnh" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/zh-CN.json b/src/i18n/resources/zh-CN.json index 74a7dd1f5f..07db7ec6ff 100644 --- a/src/i18n/resources/zh-CN.json +++ b/src/i18n/resources/zh-CN.json @@ -840,6 +840,17 @@ "visualizer-type": "可视化类型" }, "name": "可视化效果" + }, + "slack-now-playing": { + "name": "Slack 状态", + "description": "将您的 Slack 状态设置为当前正在播放的歌曲", + "status-text": "正在播放: {{artist}} - {{title}}", + "menu": { + "settings": "设置", + "token": "Slack API 令牌", + "cookie-token": "Slack Cookie 令牌", + "emoji-name": "自定义表情名称" + } } } -} +} \ No newline at end of file diff --git a/src/i18n/resources/zh-TW.json b/src/i18n/resources/zh-TW.json index cc050f05ec..9251d4e47f 100644 --- a/src/i18n/resources/zh-TW.json +++ b/src/i18n/resources/zh-TW.json @@ -840,6 +840,17 @@ "visualizer-type": "視覺化效果類型" }, "name": "視覺化效果" + }, + "slack-now-playing": { + "name": "Slack 狀態", + "description": "將您的 Slack 狀態設定為目前正在播放的歌曲", + "status-text": "正在播放: {{artist}} - {{title}}", + "menu": { + "settings": "設定", + "token": "Slack API 令牌", + "cookie-token": "Slack Cookie 令牌", + "emoji-name": "自定義表情符號名稱" + } } } -} +} \ No newline at end of file From a97018502ec2059873c7639219e0f8ba6e5caf26 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 12:05:07 -0600 Subject: [PATCH 07/29] Fix newlines --- src/i18n/resources/ar.json | 2 +- src/i18n/resources/bg.json | 2 +- src/i18n/resources/bs.json | 2 +- src/i18n/resources/ca.json | 16 ++++++++-------- src/i18n/resources/cs.json | 2 +- src/i18n/resources/da.json | 2 +- src/i18n/resources/el.json | 2 +- src/i18n/resources/et.json | 2 +- src/i18n/resources/fa.json | 2 +- src/i18n/resources/fi.json | 2 +- src/i18n/resources/fil.json | 2 +- src/i18n/resources/he.json | 2 +- src/i18n/resources/hi.json | 2 +- src/i18n/resources/hr.json | 2 +- src/i18n/resources/hu.json | 2 +- src/i18n/resources/id.json | 2 +- src/i18n/resources/is.json | 2 +- src/i18n/resources/it.json | 2 +- src/i18n/resources/ka.json | 2 +- src/i18n/resources/kn.json | 1 + src/i18n/resources/ko.json | 2 +- src/i18n/resources/lt.json | 2 +- src/i18n/resources/ml.json | 2 +- src/i18n/resources/ms.json | 2 +- src/i18n/resources/nb.json | 2 +- src/i18n/resources/ne.json | 2 +- src/i18n/resources/nl.json | 2 +- src/i18n/resources/pl.json | 2 +- src/i18n/resources/pt.json | 2 +- src/i18n/resources/ro.json | 2 +- src/i18n/resources/ru.json | 2 +- src/i18n/resources/si.json | 2 +- src/i18n/resources/sl.json | 2 +- src/i18n/resources/sr.json | 1 + src/i18n/resources/sv.json | 2 +- src/i18n/resources/ta.json | 2 +- src/i18n/resources/th.json | 2 +- src/i18n/resources/tr.json | 2 +- src/i18n/resources/uk.json | 2 +- src/i18n/resources/ur.json | 2 +- src/i18n/resources/vi.json | 2 +- src/i18n/resources/zh-CN.json | 2 +- src/i18n/resources/zh-TW.json | 2 +- 43 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/i18n/resources/ar.json b/src/i18n/resources/ar.json index 1caa81642f..1324dcc032 100644 --- a/src/i18n/resources/ar.json +++ b/src/i18n/resources/ar.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/bg.json b/src/i18n/resources/bg.json index 60a5ae02e0..d231d4a77f 100644 --- a/src/i18n/resources/bg.json +++ b/src/i18n/resources/bg.json @@ -855,7 +855,7 @@ "visualizer-type": "Тип визуализатор" }, "name": "Визуализатор" ->>>>>>> master } } } + diff --git a/src/i18n/resources/bs.json b/src/i18n/resources/bs.json index 94281adfca..d091576d0f 100644 --- a/src/i18n/resources/bs.json +++ b/src/i18n/resources/bs.json @@ -143,4 +143,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ca.json b/src/i18n/resources/ca.json index acc91310f1..e3609559e7 100644 --- a/src/i18n/resources/ca.json +++ b/src/i18n/resources/ca.json @@ -810,15 +810,15 @@ "name": "Visualitzador" }, "slack-now-playing": { - "name": "Slack Status", - "description": "Sets your Slack status to the currently playing song", - "status-text": "Now Playing: {{artist}} - {{title}}", + "name": "Estat de Slack", + "description": "Estableix el teu estat de Slack a la cançó que s'està reproduint actualment", + "status-text": "Ara sonant: {{artist}} - {{title}}", "menu": { - "settings": "Settings", - "token": "Slack API Token", - "cookie-token": "Slack Cookie Token", - "emoji-name": "Custom Emoji Name" + "settings": "Configuració", + "token": "Token API de Slack", + "cookie-token": "Token de Cookie de Slack", + "emoji-name": "Nom d'emoji personalitzat" } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/cs.json b/src/i18n/resources/cs.json index 3a8b8019fb..a1f1ef13aa 100644 --- a/src/i18n/resources/cs.json +++ b/src/i18n/resources/cs.json @@ -787,4 +787,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/da.json b/src/i18n/resources/da.json index d5afc95969..5c73a803c6 100644 --- a/src/i18n/resources/da.json +++ b/src/i18n/resources/da.json @@ -323,4 +323,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/el.json b/src/i18n/resources/el.json index 2c2f2cff2c..525cbca31f 100644 --- a/src/i18n/resources/el.json +++ b/src/i18n/resources/el.json @@ -857,4 +857,4 @@ "name": "Απεικονιστής" } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/et.json b/src/i18n/resources/et.json index 3e9502a108..31f5c4dfcf 100644 --- a/src/i18n/resources/et.json +++ b/src/i18n/resources/et.json @@ -232,4 +232,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/fa.json b/src/i18n/resources/fa.json index f3771d6fd7..544c793741 100644 --- a/src/i18n/resources/fa.json +++ b/src/i18n/resources/fa.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/fi.json b/src/i18n/resources/fi.json index e2d35b8412..bfc4dbaf87 100644 --- a/src/i18n/resources/fi.json +++ b/src/i18n/resources/fi.json @@ -661,4 +661,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/fil.json b/src/i18n/resources/fil.json index 63dad3591e..4d76e9447c 100644 --- a/src/i18n/resources/fil.json +++ b/src/i18n/resources/fil.json @@ -805,4 +805,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/he.json b/src/i18n/resources/he.json index 63a251f8b3..cfeb6acbd9 100644 --- a/src/i18n/resources/he.json +++ b/src/i18n/resources/he.json @@ -337,4 +337,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/hi.json b/src/i18n/resources/hi.json index a2945ad5e9..d2f0066119 100644 --- a/src/i18n/resources/hi.json +++ b/src/i18n/resources/hi.json @@ -363,4 +363,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/hr.json b/src/i18n/resources/hr.json index ed3ce71ffe..2f4d98170e 100644 --- a/src/i18n/resources/hr.json +++ b/src/i18n/resources/hr.json @@ -598,4 +598,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/hu.json b/src/i18n/resources/hu.json index 5ec00cb31c..7a119b3aa3 100644 --- a/src/i18n/resources/hu.json +++ b/src/i18n/resources/hu.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/id.json b/src/i18n/resources/id.json index a46d9abb60..af4e6b14a0 100644 --- a/src/i18n/resources/id.json +++ b/src/i18n/resources/id.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/is.json b/src/i18n/resources/is.json index 4a66aa1e91..9b42846eb8 100644 --- a/src/i18n/resources/is.json +++ b/src/i18n/resources/is.json @@ -821,4 +821,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/it.json b/src/i18n/resources/it.json index d99c355e2a..a6921e8c9e 100644 --- a/src/i18n/resources/it.json +++ b/src/i18n/resources/it.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ka.json b/src/i18n/resources/ka.json index cc4c174291..c1120642ef 100644 --- a/src/i18n/resources/ka.json +++ b/src/i18n/resources/ka.json @@ -316,4 +316,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/kn.json b/src/i18n/resources/kn.json index 54d4e89f59..761a933ae6 100644 --- a/src/i18n/resources/kn.json +++ b/src/i18n/resources/kn.json @@ -18,3 +18,4 @@ } } } + diff --git a/src/i18n/resources/ko.json b/src/i18n/resources/ko.json index 8146d6328d..7790f15ae4 100644 --- a/src/i18n/resources/ko.json +++ b/src/i18n/resources/ko.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/lt.json b/src/i18n/resources/lt.json index ae4b8e7f01..3cb2d599ad 100644 --- a/src/i18n/resources/lt.json +++ b/src/i18n/resources/lt.json @@ -640,4 +640,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ml.json b/src/i18n/resources/ml.json index b660d6c14a..6b173f8fb9 100644 --- a/src/i18n/resources/ml.json +++ b/src/i18n/resources/ml.json @@ -67,4 +67,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ms.json b/src/i18n/resources/ms.json index 2a1f2000b8..d9c2526035 100644 --- a/src/i18n/resources/ms.json +++ b/src/i18n/resources/ms.json @@ -211,4 +211,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/nb.json b/src/i18n/resources/nb.json index c1c897332e..ca13b3b7b0 100644 --- a/src/i18n/resources/nb.json +++ b/src/i18n/resources/nb.json @@ -599,4 +599,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ne.json b/src/i18n/resources/ne.json index 202399feb0..eeca2a3942 100644 --- a/src/i18n/resources/ne.json +++ b/src/i18n/resources/ne.json @@ -844,4 +844,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/nl.json b/src/i18n/resources/nl.json index 1df8259216..937ad173d7 100644 --- a/src/i18n/resources/nl.json +++ b/src/i18n/resources/nl.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/pl.json b/src/i18n/resources/pl.json index 7bae42d863..f033b97775 100644 --- a/src/i18n/resources/pl.json +++ b/src/i18n/resources/pl.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/pt.json b/src/i18n/resources/pt.json index e67389fb48..36bd5788a3 100644 --- a/src/i18n/resources/pt.json +++ b/src/i18n/resources/pt.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ro.json b/src/i18n/resources/ro.json index 90c247e1e5..1523c5cc01 100644 --- a/src/i18n/resources/ro.json +++ b/src/i18n/resources/ro.json @@ -843,4 +843,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ru.json b/src/i18n/resources/ru.json index d295a17f6d..f647992142 100644 --- a/src/i18n/resources/ru.json +++ b/src/i18n/resources/ru.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/si.json b/src/i18n/resources/si.json index 79345cd30b..8adf12bd0f 100644 --- a/src/i18n/resources/si.json +++ b/src/i18n/resources/si.json @@ -118,4 +118,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/sl.json b/src/i18n/resources/sl.json index 7afd05bc19..82628afe8f 100644 --- a/src/i18n/resources/sl.json +++ b/src/i18n/resources/sl.json @@ -561,4 +561,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/sr.json b/src/i18n/resources/sr.json index e53409abea..8608457f7b 100644 --- a/src/i18n/resources/sr.json +++ b/src/i18n/resources/sr.json @@ -18,3 +18,4 @@ } } } + diff --git a/src/i18n/resources/sv.json b/src/i18n/resources/sv.json index 7bd9fe3182..620146a8de 100644 --- a/src/i18n/resources/sv.json +++ b/src/i18n/resources/sv.json @@ -237,4 +237,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ta.json b/src/i18n/resources/ta.json index ffd12059a8..8f621e7edb 100644 --- a/src/i18n/resources/ta.json +++ b/src/i18n/resources/ta.json @@ -844,4 +844,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/th.json b/src/i18n/resources/th.json index 87f874fa0f..009bf782f8 100644 --- a/src/i18n/resources/th.json +++ b/src/i18n/resources/th.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/tr.json b/src/i18n/resources/tr.json index c313464d54..1d7dedd096 100644 --- a/src/i18n/resources/tr.json +++ b/src/i18n/resources/tr.json @@ -853,4 +853,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/uk.json b/src/i18n/resources/uk.json index a835db9cd2..c14a8cd7b5 100644 --- a/src/i18n/resources/uk.json +++ b/src/i18n/resources/uk.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/ur.json b/src/i18n/resources/ur.json index b37085c476..dfd49d10a0 100644 --- a/src/i18n/resources/ur.json +++ b/src/i18n/resources/ur.json @@ -214,4 +214,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/vi.json b/src/i18n/resources/vi.json index efd0d165b3..aac5a83c51 100644 --- a/src/i18n/resources/vi.json +++ b/src/i18n/resources/vi.json @@ -837,4 +837,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/zh-CN.json b/src/i18n/resources/zh-CN.json index 8f98032cc1..a671f55c86 100644 --- a/src/i18n/resources/zh-CN.json +++ b/src/i18n/resources/zh-CN.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/i18n/resources/zh-TW.json b/src/i18n/resources/zh-TW.json index 15c76f815a..38277a5acb 100644 --- a/src/i18n/resources/zh-TW.json +++ b/src/i18n/resources/zh-TW.json @@ -857,4 +857,4 @@ } } } -} \ No newline at end of file +} From 839e21a3f47842661e5e9bac2448594f10dfb7fd Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 12:09:51 -0600 Subject: [PATCH 08/29] chore: revert leftover changes to scrobbler plugin --- src/plugins/scrobbler/index.ts | 23 ----------------------- src/plugins/scrobbler/main.ts | 2 -- src/plugins/scrobbler/menu.ts | 2 -- 3 files changed, 27 deletions(-) diff --git a/src/plugins/scrobbler/index.ts b/src/plugins/scrobbler/index.ts index ef6266cc66..ceed9d7776 100644 --- a/src/plugins/scrobbler/index.ts +++ b/src/plugins/scrobbler/index.ts @@ -71,28 +71,6 @@ export interface ScrobblerPluginConfig { */ apiRoot: string; }; - slack: { - /** - * Enable Slack scrobbling - * - * @default false - */ - enabled: boolean; - /** - * Slack OAuth token - */ - token: string | undefined; - /** - * Slack cookie token (d cookie value) - */ - cookieToken: string | undefined; - /** - * Name to use for the custom emoji in Slack - * - * @default 'my-album-art' - */ - emojiName: string; - }; }; } @@ -114,7 +92,6 @@ export const defaultConfig: ScrobblerPluginConfig = { token: undefined, apiRoot: 'https://api.listenbrainz.org/1/', }, - }, }; diff --git a/src/plugins/scrobbler/main.ts b/src/plugins/scrobbler/main.ts index 2e356968ce..91a02a75ef 100644 --- a/src/plugins/scrobbler/main.ts +++ b/src/plugins/scrobbler/main.ts @@ -51,8 +51,6 @@ export const backend = createBackend< } else { this.enabledScrobblers.delete('listenbrainz'); } - - }, async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) { diff --git a/src/plugins/scrobbler/menu.ts b/src/plugins/scrobbler/menu.ts index 3dd75b26e4..08a5702d64 100644 --- a/src/plugins/scrobbler/menu.ts +++ b/src/plugins/scrobbler/menu.ts @@ -79,7 +79,6 @@ async function promptListenbrainzOptions( } } - export const onMenu = async ({ window, getConfig, @@ -148,6 +147,5 @@ export const onMenu = async ({ }, ], }, - ]; }; From 21eb5ac9e61173e83512fb81e670146aadcabc8b Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 12:33:40 -0600 Subject: [PATCH 09/29] refactor(slack-now-playing): improve type safety with proper type guards - Add isSlackNowPlayingConfig type guard function for better type checking - Replace unsafe type assertions with proper type guards --- src/plugins/slack-now-playing/main.ts | 64 +++++++++++-------- .../slack-now-playing/slack-api-client.ts | 18 +++++- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index 1e4149be41..554de078b9 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -20,6 +20,21 @@ export interface SlackNowPlayingConfig { alternativeTitles?: boolean; } +/** + * Type guard to check if an object is a valid SlackNowPlayingConfig + * @param config The object to check + * @returns True if the object is a valid SlackNowPlayingConfig + */ +function isSlackNowPlayingConfig(config: unknown): config is SlackNowPlayingConfig { + if (!config || typeof config !== 'object') return false; + + const c = config as Partial; + return typeof c.enabled === 'boolean' && + typeof c.token === 'string' && + typeof c.cookieToken === 'string' && + typeof c.emojiName === 'string'; +} + const defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; const state = { @@ -37,29 +52,29 @@ function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackN async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { try { validateConfig(config); - + // Skip if song is paused if (songInfo.isPaused) { return; } - + const title = songInfo.alternativeTitle ?? songInfo.title; const artistPart = songInfo.artist || 'Unknown Artist'; const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; - + // Use localized version of the status text let statusText = t('plugins.slack-now-playing.status-text') .replace('{{artist}}', truncatedArtist) .replace('{{title}}', title); - + // Ensure the status text doesn't exceed Slack's limit if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; - + // Calculate expiration time (current time + remaining song duration) const elapsed = songInfo.elapsedSeconds ?? 0; const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); const expirationTime = Math.floor(Date.now() / 1000) + remaining; - + await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); } catch (error) { console.error(`Error setting Slack status: ${error}`); @@ -75,23 +90,23 @@ async function updateSlackStatusWithEmoji( try { validateConfig(config); const client = new SlackApiClient(config.token, config.cookieToken); - + const statusEmoji = await getStatusEmoji(songInfo, config); - + const profileData = { status_text: statusText, status_emoji: statusEmoji, status_expiration: expirationTime, }; - + const postData = { token: config.token, profile: JSON.stringify(profileData), }; - + const res = await client.post('users.profile.set', postData); const json = res.data as SlackApiResponse; - + if (!json.ok) { console.error(`Slack API error: ${json.error}`); } else { @@ -107,7 +122,7 @@ async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig) if (songInfo.imageSrc && await uploadEmojiToSlack(songInfo, config)) { return `:${config.emojiName}:`; } - + const randomIndex = Math.floor(Math.random() * defaultEmojis.length); return defaultEmojis[randomIndex]; } @@ -181,47 +196,46 @@ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { // Skip time change events if (event === SongInfoEvent.TimeChanged) return; - + // Only update if plugin is enabled if (!config.enabled) { return; } - + // Update Slack status with current song try { - // Check if config has the expected structure - const slackConfig = config as any; - if (!slackConfig || typeof slackConfig !== 'object') { + // Check if config has the expected structure using our type guard + if (!isSlackNowPlayingConfig(config)) { return; } - + // Make sure the config has the required properties - if (!slackConfig.token || !slackConfig.cookieToken || !slackConfig.emojiName) { + if (!config.token || !config.cookieToken || !config.emojiName) { return; } - - setNowPlaying(songInfo, slackConfig) + + setNowPlaying(songInfo, config) .catch(error => console.error(`Error in Slack Now Playing: ${error}`)); } catch (error) { console.error(`Error processing song info: ${error}`); } }); }, - + async stop() { state.window = undefined; // Note: We don't unregister the callback as there's no API for that // It will be garbage collected when the plugin is unloaded }, - + async onConfigChange() { // Config changes will be picked up on the next song change }, diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index 3b6e82bf2a..491ca2944d 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -23,6 +23,7 @@ export class SlackApiClient { */ async post(endpoint: string, data: any, formData = false): Promise { const url = `https://slack.com/api/${endpoint}`; + let headers = this.getBaseHeaders(); let payload = data; if (formData) { @@ -31,7 +32,13 @@ export class SlackApiClient { headers['Content-Type'] = 'application/x-www-form-urlencoded'; payload = new URLSearchParams(data).toString(); } - return axios.post(url, payload, { headers, maxBodyLength: Infinity, validateStatus: () => true }); + + try { + return await axios.post(url, payload, { headers, maxBodyLength: Infinity, validateStatus: () => true }); + } catch (error) { + console.error(`Error in Slack API POST to ${endpoint}:`, error); + throw error; + } } /** @@ -39,8 +46,15 @@ export class SlackApiClient { */ async get(endpoint: string, params: Record = {}): Promise { const url = `https://slack.com/api/${endpoint}`; + const headers = this.getBaseHeaders(); - return axios.get(url, { headers, params, validateStatus: () => true }); + + try { + return await axios.get(url, { headers, params, validateStatus: () => true }); + } catch (error) { + console.error(`Error in Slack API GET to ${endpoint}:`, error); + throw error; + } } } From 8b757d6f46ea1f77bd1565188f27cafc69ec3337 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 12:37:54 -0600 Subject: [PATCH 10/29] feat: enhance error handling and logging in Slack Now Playing plugin --- src/plugins/slack-now-playing/main.ts | 347 ++++++++++++++++++++++---- 1 file changed, 297 insertions(+), 50 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index 554de078b9..8b26f6a563 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -49,6 +49,11 @@ function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackN } } +/** + * Updates the Slack status with the currently playing song + * @param songInfo Information about the current song + * @param config Plugin configuration + */ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { try { validateConfig(config); @@ -77,10 +82,32 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); } catch (error) { - console.error(`Error setting Slack status: ${error}`); + // Provide more detailed error information based on error type + if (error instanceof Error) { + console.error(`Error setting Slack status: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error setting Slack status: ${String(error)}`); + } + + // Re-throw specific errors that should be handled by the caller + if (error instanceof Error && + (error.message.includes('token') || error.message.includes('authentication'))) { + throw new Error('Slack authentication failed. Please check your API token and cookie token.'); + } } } +/** + * Updates the Slack status with emoji and text + * @param statusText The status text to set + * @param expirationTime When the status should expire + * @param songInfo Information about the current song + * @param config Plugin configuration + * @throws Error if the Slack API request fails + */ async function updateSlackStatusWithEmoji( statusText: string, expirationTime: number, @@ -108,13 +135,35 @@ async function updateSlackStatusWithEmoji( const json = res.data as SlackApiResponse; if (!json.ok) { - console.error(`Slack API error: ${json.error}`); + // Handle specific API error codes + const errorMessage = `Slack API error: ${json.error}`; + console.error(errorMessage, { response: json }); + + // Throw specific errors based on the error type + if (json.error === 'invalid_auth' || json.error === 'token_expired') { + throw new Error('Slack authentication failed. Please check your API token and cookie token.'); + } else if (json.error === 'rate_limited') { + throw new Error('Slack API rate limit exceeded. Please try again later.'); + } else { + throw new Error(errorMessage); + } } else { state.lastStatus = statusText; state.lastEmoji = statusEmoji; } } catch (error) { - console.error(`Error updating Slack status: ${error}`); + // Provide more detailed error information based on error type + if (error instanceof Error) { + console.error(`Error updating Slack status: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error updating Slack status: ${String(error)}`); + } + + // Re-throw the error to be handled by the caller + throw error; } } @@ -127,73 +176,237 @@ async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig) return defaultEmojis[randomIndex]; } +/** + * Uploads album art to Slack as a custom emoji + * @param songInfo Information about the current song + * @param config Plugin configuration + * @returns True if the emoji was successfully uploaded, false otherwise + */ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { - validateConfig(config); - const client = new SlackApiClient(config.token, config.cookieToken); - const filePath = await saveAlbumArtToFile(songInfo); - if (!filePath) return false; - const emojiDeleted = await ensureEmojiDoesNotExist(config); - if (!emojiDeleted) return false; - const formData = new FormData(); - formData.append('token', config.token); - formData.append('mode', 'data'); - formData.append('name', config.emojiName); - const fileBuffer = fs.readFileSync(filePath); - formData.append('image', fileBuffer, { - filename: 'album-art.jpg', - contentType: 'image/jpeg', - }); - const res = await client.post('emoji.add', formData, true); - const json = res.data as SlackApiResponse; - if (json.ok) return true; - console.error(`Error uploading emoji: ${json.error}`); - return false; + try { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + + // Save album art to a temporary file + const filePath = await saveAlbumArtToFile(songInfo); + if (!filePath) { + console.warn('Failed to save album art to file'); + return false; + } + + // Make sure the emoji doesn't already exist + const emojiDeleted = await ensureEmojiDoesNotExist(config); + if (!emojiDeleted) { + console.warn('Failed to ensure emoji does not exist'); + return false; + } + + // Prepare the form data for the API request + const formData = new FormData(); + formData.append('token', config.token); + formData.append('mode', 'data'); + formData.append('name', config.emojiName); + + // Read the file and add it to the form data + try { + const fileBuffer = fs.readFileSync(filePath); + formData.append('image', fileBuffer, { + filename: 'album-art.jpg', + contentType: 'image/jpeg', + }); + } catch (fileError) { + console.error(`Error reading album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); + return false; + } + + // Make the API request + const res = await client.post('emoji.add', formData, true); + const json = res.data as SlackApiResponse; + + if (json.ok) return true; + + // Handle specific API error codes + if (json.error === 'invalid_name') { + console.error(`Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`); + } else if (json.error === 'too_large') { + console.error('Album art image is too large for Slack emoji (max 128KB).'); + } else { + console.error(`Error uploading emoji: ${json.error}`); + } + + return false; + } catch (error) { + if (error instanceof Error) { + console.error(`Error in uploadEmojiToSlack: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error in uploadEmojiToSlack: ${String(error)}`); + } + return false; + } } +/** + * Downloads and saves album art to a temporary file + * @param songInfo Information about the current song + * @returns Path to the saved file, or null if the operation failed + */ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { - if (!songInfo.imageSrc) return null; + if (!songInfo.imageSrc) { + console.warn('No image source available for album art'); + return null; + } + try { const tempDir = os.tmpdir(); const filePath = path.join(tempDir, 'album-art.jpg'); - const response = await net.fetch(songInfo.imageSrc); - const imageBuffer = Buffer.from(await response.arrayBuffer()); - fs.writeFileSync(filePath, imageBuffer); - return filePath; + + // Fetch the image + let response: Response; + try { + response = await net.fetch(songInfo.imageSrc); + + if (!response.ok) { + console.error(`Failed to fetch album art: HTTP ${response.status} ${response.statusText}`); + return null; + } + } catch (fetchError) { + console.error(`Network error fetching album art: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); + return null; + } + + // Convert the response to a buffer + let imageBuffer: Buffer; + try { + imageBuffer = Buffer.from(await response.arrayBuffer()); + + if (imageBuffer.length === 0) { + console.error('Received empty album art image'); + return null; + } + } catch (bufferError) { + console.error(`Error processing album art data: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); + return null; + } + + // Write the buffer to a file + try { + fs.writeFileSync(filePath, imageBuffer); + return filePath; + } catch (fileError) { + console.error(`Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); + return null; + } } catch (error) { - console.error(`Error saving album art to file: ${error}`); + // Catch any other unexpected errors + if (error instanceof Error) { + console.error(`Error saving album art to file: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error saving album art to file: ${String(error)}`); + } return null; } } +/** + * Checks if the emoji already exists and deletes it if necessary + * @param config Plugin configuration + * @returns True if the emoji doesn't exist or was successfully deleted, false otherwise + */ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { - validateConfig(config); - const client = new SlackApiClient(config.token, config.cookieToken); - const res = await client.get('emoji.list', { token: config.token }); - const json = res.data as SlackApiResponse; - if (json.ok) { - if (json.emoji && json.emoji[config.emojiName]) { - return await deleteExistingEmoji(config); + try { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + + // Get the list of emojis + const res = await client.get('emoji.list', { token: config.token }); + const json = res.data as SlackApiResponse; + + if (json.ok) { + // Check if the emoji exists + if (json.emoji && json.emoji[config.emojiName]) { + return await deleteExistingEmoji(config); + } else { + // Emoji doesn't exist, no need to delete + return true; + } } else { - return true; + // Handle specific API error codes + if (json.error === 'invalid_auth' || json.error === 'token_expired') { + console.error('Slack authentication failed. Please check your API token and cookie token.'); + } else if (json.error === 'rate_limited') { + console.error('Slack API rate limit exceeded. Please try again later.'); + } else { + console.error(`Error checking emoji list: ${json.error}`); + } + return false; + } + } catch (error) { + if (error instanceof Error) { + console.error(`Error in ensureEmojiDoesNotExist: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error in ensureEmojiDoesNotExist: ${String(error)}`); } - } else { - console.error(`Error checking emoji list: ${json.error}`); return false; } } +/** + * Deletes an existing emoji from Slack + * @param config Plugin configuration + * @returns True if the emoji was successfully deleted or doesn't exist, false otherwise + */ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { - validateConfig(config); - const client = new SlackApiClient(config.token, config.cookieToken); - const data = { token: config.token, name: config.emojiName }; - const res = await client.post('emoji.remove', data); - const json = res.data as SlackApiResponse; - if (json.ok || json.error === 'emoji_not_found') return true; - console.error(`Error deleting emoji: ${json.error}`); - return false; + try { + validateConfig(config); + const client = new SlackApiClient(config.token, config.cookieToken); + + // Delete the emoji + const data = { token: config.token, name: config.emojiName }; + const res = await client.post('emoji.remove', data); + const json = res.data as SlackApiResponse; + + // Consider both success and 'emoji_not_found' as successful outcomes + if (json.ok || json.error === 'emoji_not_found') { + return true; + } + + // Handle specific API error codes + if (json.error === 'invalid_auth' || json.error === 'token_expired') { + console.error('Slack authentication failed. Please check your API token and cookie token.'); + } else if (json.error === 'rate_limited') { + console.error('Slack API rate limit exceeded. Please try again later.'); + } else { + console.error(`Error deleting emoji: ${json.error}`); + } + + return false; + } catch (error) { + if (error instanceof Error) { + console.error(`Error in deleteExistingEmoji: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error in deleteExistingEmoji: ${String(error)}`); + } + return false; + } } export const backend = createBackend({ + /** + * Start the Slack Now Playing plugin + * @param ctx The plugin context + */ async start(ctx) { state.window = ctx.window; @@ -214,18 +427,52 @@ export const backend = createBackend({ try { // Check if config has the expected structure using our type guard if (!isSlackNowPlayingConfig(config)) { + // Log a warning only on the first occurrence to avoid spamming the console + if (!state.lastStatus) { + console.warn('Invalid Slack Now Playing configuration'); + } return; } // Make sure the config has the required properties if (!config.token || !config.cookieToken || !config.emojiName) { + // Log a warning only on the first occurrence to avoid spamming the console + if (!state.lastStatus) { + console.warn('Missing required Slack Now Playing configuration values'); + } return; } - setNowPlaying(songInfo, config) - .catch(error => console.error(`Error in Slack Now Playing: ${error}`)); + // Update the Slack status + setNowPlaying(songInfo, config).catch(error => { + // Handle specific error types + if (error instanceof Error) { + // Check for authentication errors + if (error.message.includes('authentication') || error.message.includes('token')) { + console.error('Slack authentication failed. Please check your API token and cookie token.'); + } + // Check for rate limiting errors + else if (error.message.includes('rate limit') || error.message.includes('rate_limited')) { + console.error('Slack API rate limit exceeded. Please try again later.'); + } + // Generic error handling + else { + console.error(`Error in Slack Now Playing: ${error.message}`); + } + } else { + console.error(`Error in Slack Now Playing: ${String(error)}`); + } + }); } catch (error) { - console.error(`Error processing song info: ${error}`); + // Handle unexpected errors in the callback itself + if (error instanceof Error) { + console.error(`Error processing song info: ${error.message}`, { + name: error.name, + stack: error.stack + }); + } else { + console.error(`Error processing song info: ${String(error)}`); + } } }); }, From 38d964ac05101882106af5ccd404d6a4e23f9741 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 14:12:06 -0600 Subject: [PATCH 11/29] feat: add album art caching and temporary file cleanup for Slack plugin --- src/plugins/slack-now-playing/main.ts | 391 ++++++++++++++++++++++---- src/plugins/slack-now-playing/menu.ts | 115 +++++++- 2 files changed, 445 insertions(+), 61 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index 8b26f6a563..a89529219d 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -37,15 +37,144 @@ function isSlackNowPlayingConfig(config: unknown): config is SlackNowPlayingConf const defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; +// Cache to store album art file paths by URL to avoid repeated downloads +type AlbumArtCache = { + [url: string]: { + filePath: string; + timestamp: number; + }; +}; + const state = { lastStatus: '', lastEmoji: '', window: undefined as Electron.BrowserWindow | undefined, + tempFiles: new Set(), // Track temporary files for cleanup + albumArtCache: {} as AlbumArtCache, // Cache album art files + cacheExpiryMs: 30 * 60 * 1000, // Cache expiry time (30 minutes) + cacheCleanupTimer: undefined as NodeJS.Timeout | undefined, // Timer for periodic cache cleanup + context: undefined as any, // Store the plugin context + currentConfig: undefined as SlackNowPlayingConfig | undefined, // Current configuration +}; + +/** + * Register a temporary file for cleanup when the plugin is stopped + * @param filePath Path to the temporary file + */ +function registerFileForCleanup(filePath: string): void { + state.tempFiles.add(filePath); +} + +/** + * Clean up all temporary files created by the plugin + */ +async function cleanupTempFiles(): Promise { + const fsPromises = fs.promises; + + for (const filePath of state.tempFiles) { + try { + // Check if the file exists before attempting to delete it + await fsPromises.access(filePath, fs.constants.F_OK) + .then(() => fsPromises.unlink(filePath)) + .then(() => { + // Remove the file from the set once it's deleted + state.tempFiles.delete(filePath); + }) + .catch((error: NodeJS.ErrnoException) => { + // Ignore errors if the file doesn't exist + if (error.code !== 'ENOENT') { + console.error(`Error deleting temporary file ${filePath}:`, error); + } + }); + } catch (error) { + // Catch any unexpected errors + if (error instanceof Error) { + console.error(`Error during cleanup of ${filePath}:`, error.message); + } else { + console.error(`Error during cleanup of ${filePath}:`, String(error)); + } + } + } +} + +/** + * Clean up expired cache entries to prevent the cache from growing too large + */ +async function cleanupExpiredCache(): Promise { + const now = Date.now(); + const fsPromises = fs.promises; + + // Check each cache entry + for (const [url, cacheEntry] of Object.entries(state.albumArtCache)) { + // If the entry is expired + if (now - cacheEntry.timestamp > state.cacheExpiryMs) { + // Remove from cache + delete state.albumArtCache[url]; + + // Try to delete the file if it's not needed elsewhere + try { + await fsPromises.access(cacheEntry.filePath, fs.constants.F_OK); + await fsPromises.unlink(cacheEntry.filePath); + state.tempFiles.delete(cacheEntry.filePath); + } catch (error) { + // Ignore errors if the file doesn't exist or is in use + } + } + } +} + +/** + * Result of configuration validation + */ +type ValidationResult = { + valid: boolean; + errors: string[]; }; -function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackNowPlayingConfig { - if (!config.token || !config.cookieToken || !config.emojiName) { - throw new Error('Missing Slack config values'); +/** + * Validates the Slack Now Playing configuration + * @param config The configuration to validate + * @returns A validation result object + */ +function validateConfig(config: SlackNowPlayingConfig): ValidationResult { + const errors: string[] = []; + + // Check token + if (!config.token) { + errors.push('Missing Slack API token'); + } else if (!config.token.startsWith('xoxc-')) { + errors.push('Invalid Slack API token format (should start with "xoxc-")'); + } + + // Check cookie token + if (!config.cookieToken) { + errors.push('Missing Slack cookie token'); + } else if (!config.cookieToken.startsWith('xoxd-')) { + errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); + } + + // Check emoji name + if (!config.emojiName) { + errors.push('Missing custom emoji name'); + } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { + errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); + } + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * Validates the configuration and throws an error if invalid + * @param config The configuration to validate + * @throws Error if the configuration is invalid + */ +function assertValidConfig(config: SlackNowPlayingConfig): asserts config is SlackNowPlayingConfig { + const result = validateConfig(config); + if (!result.valid) { + throw new Error(`Invalid Slack Now Playing configuration: ${result.errors.join(', ')}`); } } @@ -56,7 +185,12 @@ function validateConfig(config: SlackNowPlayingConfig): asserts config is SlackN */ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { try { - validateConfig(config); + // Validate configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { + console.error(`Cannot set Slack status: ${validationResult.errors.join(', ')}`); + return; + } // Skip if song is paused if (songInfo.isPaused) { @@ -115,7 +249,12 @@ async function updateSlackStatusWithEmoji( config: SlackNowPlayingConfig, ) { try { - validateConfig(config); + // Validate configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { + throw new Error(`Cannot update Slack status: ${validationResult.errors.join(', ')}`); + } + const client = new SlackApiClient(config.token, config.cookieToken); const statusEmoji = await getStatusEmoji(songInfo, config); @@ -184,7 +323,13 @@ async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig) */ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { try { - validateConfig(config); + // Validate configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { + console.error(`Cannot upload emoji to Slack: ${validationResult.errors.join(', ')}`); + return false; + } + const client = new SlackApiClient(config.token, config.cookieToken); // Save album art to a temporary file @@ -260,13 +405,47 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { } try { + const imageUrl = songInfo.imageSrc; + const now = Date.now(); + + // Check if we have a cached version of this image + const cachedImage = state.albumArtCache[imageUrl]; + if (cachedImage) { + // Check if the cached file exists and is not expired + const cacheAge = now - cachedImage.timestamp; + if (cacheAge < state.cacheExpiryMs) { + try { + // Verify the file still exists + await fs.promises.access(cachedImage.filePath, fs.constants.F_OK); + return cachedImage.filePath; + } catch (error) { + // File doesn't exist anymore, remove from cache + delete state.albumArtCache[imageUrl]; + } + } else { + // Cache entry expired, remove it + delete state.albumArtCache[imageUrl]; + // Try to clean up the old file + try { + await fs.promises.unlink(cachedImage.filePath); + state.tempFiles.delete(cachedImage.filePath); + } catch (error) { + // Ignore errors if the file doesn't exist + } + } + } + + // Create a unique filename to prevent conflicts const tempDir = os.tmpdir(); - const filePath = path.join(tempDir, 'album-art.jpg'); + const timestamp = now; + const randomString = Math.random().toString(36).substring(2, 10); + const filename = `album-art-${timestamp}-${randomString}.jpg`; + const filePath = path.join(tempDir, filename); // Fetch the image let response: Response; try { - response = await net.fetch(songInfo.imageSrc); + response = await net.fetch(imageUrl); if (!response.ok) { console.error(`Failed to fetch album art: HTTP ${response.status} ${response.statusText}`); @@ -291,9 +470,21 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { return null; } - // Write the buffer to a file + // Write the buffer to a file using async file operations try { - fs.writeFileSync(filePath, imageBuffer); + // Import the promises API from fs + const fsPromises = fs.promises; + await fsPromises.writeFile(filePath, imageBuffer); + + // Register the file for cleanup when the app exits + registerFileForCleanup(filePath); + + // Add to cache + state.albumArtCache[imageUrl] = { + filePath, + timestamp: now + }; + return filePath; } catch (fileError) { console.error(`Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); @@ -320,7 +511,13 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { */ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { try { - validateConfig(config); + // Validate configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { + console.error(`Cannot check emoji existence: ${validationResult.errors.join(', ')}`); + return false; + } + const client = new SlackApiClient(config.token, config.cookieToken); // Get the list of emojis @@ -366,7 +563,13 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { try { - validateConfig(config); + // Validate configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { + console.error(`Cannot delete emoji: ${validationResult.errors.join(', ')}`); + return false; + } + const client = new SlackApiClient(config.token, config.cookieToken); // Delete the emoji @@ -402,67 +605,120 @@ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { + // Use synchronous operations for the exit event + for (const filePath of state.tempFiles) { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (error) { + // Can't log during exit event, but we tried our best to clean up + } + } + }); + + // Handle other termination signals + ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => { + process.on(signal, async () => { + await cleanupTempFiles(); + process.exit(0); + }); + }); +} + export const backend = createBackend({ /** * Start the Slack Now Playing plugin * @param ctx The plugin context */ async start(ctx) { + // Store the context and window for later use + state.context = ctx; state.window = ctx.window; + + // Register exit handlers for cleanup + registerExitHandlers(); + + // Set up periodic cache cleanup (every hour) + const cacheCleanupInterval = 60 * 60 * 1000; // 1 hour in milliseconds + const cacheCleanupTimer = setInterval(async () => { + try { + await cleanupExpiredCache(); + } catch (error) { + // Ignore errors in the background task + } + }, cacheCleanupInterval); + + // Store the timer so we can clear it when the plugin stops + state.cacheCleanupTimer = cacheCleanupTimer; - // Get the config - const config = await ctx.getConfig(); + // Get the initial config and store it + const initialConfig = await ctx.getConfig(); + state.currentConfig = initialConfig as SlackNowPlayingConfig; // Register callback to listen for song changes - registerCallback((songInfo, event) => { + registerCallback(async (songInfo, event) => { // Skip time change events if (event === SongInfoEvent.TimeChanged) return; - - // Only update if plugin is enabled - if (!config.enabled) { - return; - } - - // Update Slack status with current song + try { + // Get the latest config each time + const latestConfig = await ctx.getConfig(); + const config = latestConfig as SlackNowPlayingConfig; + state.currentConfig = config; // Update stored config + + // Only update if plugin is enabled + if (!config.enabled) { + return; + } + + // Update Slack status with current song // Check if config has the expected structure using our type guard if (!isSlackNowPlayingConfig(config)) { // Log a warning only on the first occurrence to avoid spamming the console if (!state.lastStatus) { - console.warn('Invalid Slack Now Playing configuration'); + console.warn('Invalid Slack Now Playing configuration structure'); } return; } - - // Make sure the config has the required properties - if (!config.token || !config.cookieToken || !config.emojiName) { + + // Validate the configuration + const validationResult = validateConfig(config); + if (!validationResult.valid) { // Log a warning only on the first occurrence to avoid spamming the console if (!state.lastStatus) { - console.warn('Missing required Slack Now Playing configuration values'); + console.warn(`Slack Now Playing configuration validation failed: ${validationResult.errors.join(', ')}`); } return; } - // Update the Slack status - setNowPlaying(songInfo, config).catch(error => { - // Handle specific error types - if (error instanceof Error) { - // Check for authentication errors - if (error.message.includes('authentication') || error.message.includes('token')) { - console.error('Slack authentication failed. Please check your API token and cookie token.'); - } - // Check for rate limiting errors - else if (error.message.includes('rate limit') || error.message.includes('rate_limited')) { - console.error('Slack API rate limit exceeded. Please try again later.'); - } - // Generic error handling - else { - console.error(`Error in Slack Now Playing: ${error.message}`); + // Process the song info with the latest config + await setNowPlaying(songInfo, config) + .catch(error => { + // Handle specific error types + if (error instanceof Error) { + // Check for authentication errors + if (error.message.includes('authentication') || error.message.includes('token')) { + console.error('Slack authentication failed. Please check your API token and cookie token.'); + } + // Check for rate limiting errors + else if (error.message.includes('rate limit') || error.message.includes('rate_limited')) { + console.error('Slack API rate limit exceeded. Please try again later.'); + } + // Generic error handling + else { + console.error(`Error in Slack Now Playing: ${error.message}`); + } + } else { + console.error(`Error in Slack Now Playing: ${String(error)}`); } - } else { - console.error(`Error in Slack Now Playing: ${String(error)}`); - } - }); + }); } catch (error) { // Handle unexpected errors in the callback itself if (error instanceof Error) { @@ -477,13 +733,54 @@ export const backend = createBackend({ }); }, + /** + * Stop the Slack Now Playing plugin and clean up resources + */ async stop() { + // Clear the cache cleanup timer + if (state.cacheCleanupTimer) { + clearInterval(state.cacheCleanupTimer); + state.cacheCleanupTimer = undefined; + } + + // Clean up any temporary files created by the plugin + await cleanupTempFiles(); + + // Run a final cache cleanup + await cleanupExpiredCache(); + + // Clear the window reference state.window = undefined; + // Note: We don't unregister the callback as there's no API for that // It will be garbage collected when the plugin is unloaded }, + /** + * Handle configuration changes + * This is called when the user updates the plugin configuration + */ async onConfigChange() { - // Config changes will be picked up on the next song change + if (state.context) { + try { + // Get the latest configuration + const latestConfig = await state.context.getConfig(); + const config = latestConfig as SlackNowPlayingConfig; + + // Update the stored configuration + state.currentConfig = config; + + // Validate the configuration + try { + // Use assertValidConfig to validate the configuration + assertValidConfig(config); + console.log('Slack Now Playing configuration updated successfully'); + } catch (error) { + console.warn(`Slack Now Playing configuration validation failed: ${error instanceof Error ? error.message : String(error)}`); + } + } catch (error) { + console.error('Error updating Slack Now Playing configuration:', error); + } + } }, }); diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts index ddc18cdf55..59be766c2b 100644 --- a/src/plugins/slack-now-playing/menu.ts +++ b/src/plugins/slack-now-playing/menu.ts @@ -1,5 +1,5 @@ import prompt from 'custom-electron-prompt'; -import { BrowserWindow } from 'electron'; +import { BrowserWindow, dialog } from 'electron'; import promptOptions from '@/providers/prompt-options'; import { t } from '@/i18n'; @@ -7,14 +7,61 @@ import type { SlackNowPlayingConfig } from './main'; import type { MenuContext } from '@/types/contexts'; import type { MenuTemplate } from '@/menu'; +/** + * Result of configuration validation + */ +type ValidationResult = { + valid: boolean; + errors: string[]; +}; + +/** + * Validates the Slack Now Playing configuration + * @param config The configuration to validate + * @returns A validation result object + */ +function validateConfig(config: SlackNowPlayingConfig): ValidationResult { + const errors: string[] = []; + + // Check token + if (!config.token) { + errors.push('Missing Slack API token'); + } else if (!config.token.startsWith('xoxc-')) { + errors.push('Invalid Slack API token format (should start with "xoxc-")'); + } + + // Check cookie token + if (!config.cookieToken) { + errors.push('Missing Slack cookie token'); + } else if (!config.cookieToken.startsWith('xoxd-')) { + errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); + } + + // Check emoji name + if (!config.emojiName) { + errors.push('Missing custom emoji name'); + } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { + errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); + } + + return { + valid: errors.length === 0, + errors + }; +} + /** * Prompts user for Slack Now Playing plugin settings + * @param options Current plugin configuration + * @param setConfig Function to save the updated configuration + * @param window Browser window instance + * @returns Promise that resolves when the configuration is saved */ async function promptSlackNowPlayingOptions( options: SlackNowPlayingConfig, setConfig: (config: SlackNowPlayingConfig) => void, window: BrowserWindow, -) { +): Promise { const output = await prompt( { title: t('plugins.slack-now-playing.name'), @@ -26,6 +73,7 @@ async function promptSlackNowPlayingOptions( value: options.token, inputAttrs: { type: 'text', + placeholder: 'xoxc-...', }, }, { @@ -33,6 +81,7 @@ async function promptSlackNowPlayingOptions( value: options.cookieToken, inputAttrs: { type: 'text', + placeholder: 'xoxd-...', }, }, { @@ -40,6 +89,7 @@ async function promptSlackNowPlayingOptions( value: options.emojiName, inputAttrs: { type: 'text', + placeholder: 'my-album-art', }, }, ], @@ -51,16 +101,52 @@ async function promptSlackNowPlayingOptions( ); if (output) { - if (output[0]) { - options.token = output[0]; - } - if (output[1]) { - options.cookieToken = output[1]; - } - if (output[2]) { - options.emojiName = output[2]; + try { + // Create a deep copy of the options to ensure we don't modify the original + const updatedOptions = { ...options } as SlackNowPlayingConfig; + + // Update only the fields that were provided + if (output[0] !== undefined) { + updatedOptions.token = output[0]; + } + if (output[1] !== undefined) { + updatedOptions.cookieToken = output[1]; + } + if (output[2] !== undefined) { + updatedOptions.emojiName = output[2]; + } + + // Validate the updated options + const validationResult = validateConfig(updatedOptions); + + if (!validationResult.valid) { + // Show validation errors to the user + await dialog.showMessageBox(window, { + type: 'warning', + title: 'Slack Now Playing Configuration Issues', + message: 'There are issues with your Slack configuration:', + detail: validationResult.errors.join('\n'), + buttons: ['OK'], + }); + } + + // Save the config even if it has validation errors + // This allows users to save partial configurations + await setConfig(updatedOptions); + + // Verify the config was saved by getting it again + // This is just for debugging purposes + console.log('Saved Slack Now Playing configuration:', updatedOptions); + } catch (error) { + console.error('Error saving Slack Now Playing configuration:', error); + await dialog.showMessageBox(window, { + type: 'error', + title: 'Configuration Error', + message: 'Failed to save Slack Now Playing configuration', + detail: error instanceof Error ? error.message : String(error), + buttons: ['OK'], + }); } - setConfig(options); } } @@ -69,12 +155,13 @@ export const onMenu = async ({ getConfig, setConfig, }: MenuContext): Promise => { - const config = await getConfig(); return [ { label: t('plugins.slack-now-playing.menu.settings'), - click() { - promptSlackNowPlayingOptions(config, setConfig, window); + async click() { + // Get the latest config before showing the prompt + const latestConfig = await getConfig(); + await promptSlackNowPlayingOptions(latestConfig, setConfig, window); }, }, ]; From 7c468dc309325e2edd2befdff1a17bb8138e040a Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 14:22:44 -0600 Subject: [PATCH 12/29] refactor: improve Slack API error handling and response typing --- src/plugins/slack-now-playing/main.ts | 220 +++++++++-------- .../slack-now-playing/slack-api-client.ts | 227 ++++++++++++++++-- 2 files changed, 329 insertions(+), 118 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index a89529219d..d6cc2a85de 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -2,7 +2,7 @@ import { net } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import { SlackApiClient, SlackApiResponse } from './slack-api-client'; +import { SlackApiClient, SlackApiError } from './slack-api-client'; import FormData from 'form-data'; import { createBackend } from '@/utils'; import registerCallback, { SongInfoEvent } from '@/providers/song-info'; @@ -247,7 +247,7 @@ async function updateSlackStatusWithEmoji( expirationTime: number, songInfo: SongInfo, config: SlackNowPlayingConfig, -) { +): Promise { try { // Validate configuration const validationResult = validateConfig(config); @@ -257,42 +257,39 @@ async function updateSlackStatusWithEmoji( const client = new SlackApiClient(config.token, config.cookieToken); + // Get the appropriate emoji for the current song const statusEmoji = await getStatusEmoji(songInfo, config); - const profileData = { - status_text: statusText, - status_emoji: statusEmoji, - status_expiration: expirationTime, + // Prepare the status update payload + const statusUpdatePayload = { + profile: JSON.stringify({ + status_text: statusText, + status_emoji: statusEmoji, + status_expiration: expirationTime, + }), }; - const postData = { - token: config.token, - profile: JSON.stringify(profileData), - }; - - const res = await client.post('users.profile.set', postData); - const json = res.data as SlackApiResponse; - - if (!json.ok) { - // Handle specific API error codes - const errorMessage = `Slack API error: ${json.error}`; - console.error(errorMessage, { response: json }); - - // Throw specific errors based on the error type - if (json.error === 'invalid_auth' || json.error === 'token_expired') { - throw new Error('Slack authentication failed. Please check your API token and cookie token.'); - } else if (json.error === 'rate_limited') { - throw new Error('Slack API rate limit exceeded. Please try again later.'); - } else { - throw new Error(errorMessage); - } - } else { - state.lastStatus = statusText; - state.lastEmoji = statusEmoji; - } - } catch (error) { - // Provide more detailed error information based on error type - if (error instanceof Error) { + // Update the status + // The client now handles API errors internally + await client.post('users.profile.set', statusUpdatePayload); + + // Update state with the new status and emoji + state.lastStatus = statusText; + state.lastEmoji = statusEmoji; + + // Log success + console.log(`Slack status updated successfully: ${statusText} ${statusEmoji}`); + } catch (error: unknown) { + // Handle SlackApiError specifically + if (error instanceof SlackApiError) { + console.error(`Slack API error updating status: ${error.message}`, { + endpoint: error.endpoint, + statusCode: error.statusCode, + responseError: error.responseData?.error, + }); + } + // Handle other errors + else if (error instanceof Error) { console.error(`Error updating Slack status: ${error.message}`, { name: error.name, stack: error.stack @@ -348,13 +345,14 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon // Prepare the form data for the API request const formData = new FormData(); - formData.append('token', config.token); + // We don't need to include the token in the form data anymore as it's in the headers formData.append('mode', 'data'); formData.append('name', config.emojiName); - // Read the file and add it to the form data + // Read the file and add it to the form data using async/await pattern try { - const fileBuffer = fs.readFileSync(filePath); + // Use async file operations instead of synchronous ones + const fileBuffer = await fs.promises.readFile(filePath); formData.append('image', fileBuffer, { filename: 'album-art.jpg', contentType: 'image/jpeg', @@ -364,35 +362,46 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon return false; } - // Make the API request - const res = await client.post('emoji.add', formData, true); - const json = res.data as SlackApiResponse; - - if (json.ok) return true; - - // Handle specific API error codes - if (json.error === 'invalid_name') { - console.error(`Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`); - } else if (json.error === 'too_large') { - console.error('Album art image is too large for Slack emoji (max 128KB).'); - } else { - console.error(`Error uploading emoji: ${json.error}`); + // Make the API request - the client now handles API errors internally + try { + // The post method now returns a properly typed response + await client.post<{ ok: boolean }>('emoji.add', formData, true); + + // If we got here, the request was successful + console.log(`Successfully uploaded emoji: ${config.emojiName}`); + return true; + } catch (apiError) { + // Handle specific API error types + if (apiError instanceof SlackApiError && apiError.responseData) { + const errorCode = apiError.responseData.error; + + if (errorCode === 'invalid_name') { + console.error(`Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`); + } else if (errorCode === 'too_large') { + console.error('Album art image is too large for Slack emoji (max 128KB).'); + } else if (errorCode === 'name_taken') { + console.error(`Emoji name '${config.emojiName}' is already taken. This should not happen as we check for existing emojis.`); + } else { + console.error(`Error uploading emoji: ${errorCode}`, apiError.responseData); + } + } else { + console.error(`Error uploading emoji to Slack: ${apiError instanceof Error ? apiError.message : String(apiError)}`); + } + return false; } - - return false; - } catch (error) { + } catch (error: unknown) { + // Handle any other unexpected errors if (error instanceof Error) { - console.error(`Error in uploadEmojiToSlack: ${error.message}`, { + console.error(`Unexpected error uploading emoji to Slack: ${error.message}`, { name: error.name, stack: error.stack }); } else { - console.error(`Error in uploadEmojiToSlack: ${String(error)}`); + console.error(`Unexpected error uploading emoji to Slack: ${String(error)}`); } return false; } } - /** * Downloads and saves album art to a temporary file * @param songInfo Information about the current song @@ -520,37 +529,47 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise }>('emoji.list'); + const emojiList = response.data; + + // Check if our emoji exists + if (emojiList && emojiList.emoji && emojiList.emoji[config.emojiName]) { + console.log(`Emoji '${config.emojiName}' already exists, attempting to delete it`); return await deleteExistingEmoji(config); } else { // Emoji doesn't exist, no need to delete + console.log(`Emoji '${config.emojiName}' doesn't exist, no need to delete`); return true; } - } else { - // Handle specific API error codes - if (json.error === 'invalid_auth' || json.error === 'token_expired') { - console.error('Slack authentication failed. Please check your API token and cookie token.'); - } else if (json.error === 'rate_limited') { - console.error('Slack API rate limit exceeded. Please try again later.'); + } catch (apiError) { + // Handle specific API error types + if (apiError instanceof SlackApiError) { + const errorCode = apiError.responseData?.error; + + if (errorCode === 'invalid_auth' || errorCode === 'token_expired') { + console.error('Slack authentication failed. Please check your API token and cookie token.'); + } else if (errorCode === 'rate_limited') { + console.error('Slack API rate limit exceeded. Please try again later.'); + } else { + console.error(`Error checking emoji list: ${errorCode || apiError.message}`); + } } else { - console.error(`Error checking emoji list: ${json.error}`); + console.error(`Error checking emoji list: ${apiError instanceof Error ? apiError.message : String(apiError)}`); } return false; } - } catch (error) { + } catch (error: unknown) { + // Handle any other unexpected errors if (error instanceof Error) { - console.error(`Error in ensureEmojiDoesNotExist: ${error.message}`, { + console.error(`Unexpected error in ensureEmojiDoesNotExist: ${error.message}`, { name: error.name, stack: error.stack }); } else { - console.error(`Error in ensureEmojiDoesNotExist: ${String(error)}`); + console.error(`Unexpected error in ensureEmojiDoesNotExist: ${String(error)}`); } return false; } @@ -572,34 +591,47 @@ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { + /** Whether the API call was successful */ + ok: boolean; + /** Error code if the call failed */ + error?: string; + /** Error description if available */ + error_description?: string; + /** Warning messages from the API */ + warning?: string; + /** Response data properties */ + [key: string]: any; + /** Typed response data (available when ok is true) */ + data?: TData; +} + +/** + * Parameters for Slack API requests + */ +export type SlackApiParams = Record; + +/** + * Error thrown by the Slack API client + */ +export class SlackApiError extends Error { + /** The original error that caused this error */ + readonly originalError: Error; + /** The endpoint that was called */ + readonly endpoint: string; + /** The HTTP status code if available */ + readonly statusCode?: number; + /** The response data if available */ + readonly responseData?: SlackApiResponse; + + /** + * Create a new Slack API error + * @param message Error message + * @param endpoint The API endpoint that was called + * @param originalError The original error that caused this error + * @param statusCode The HTTP status code if available + * @param responseData The response data if available + */ + constructor( + message: string, + endpoint: string, + originalError: Error, + statusCode?: number, + responseData?: SlackApiResponse + ) { + super(message); + this.name = 'SlackApiError'; + this.originalError = originalError; + this.endpoint = endpoint; + this.statusCode = statusCode; + this.responseData = responseData; + } +} + +/** + * Centralized Slack API client for making requests to the Slack API + * + * This client handles authentication, error handling, and request formatting + * for all Slack API calls in the application. */ export class SlackApiClient { + /** The Slack API token (xoxc-) */ readonly token: string; + /** The Slack cookie token (xoxd-) */ readonly cookie: string; + /** Base URL for all Slack API requests */ + private readonly baseUrl = 'https://slack.com/api'; + /** + * Create a new Slack API client + * @param token The Slack API token (xoxc-) + * @param cookie The Slack cookie token (xoxd-) + */ constructor(token: string, cookie: string) { this.token = token; this.cookie = cookie; } + /** + * Get the base headers required for all Slack API requests + * @returns Headers object with authentication information + */ private getBaseHeaders(): Record { return { 'Cookie': `d=${this.cookie}`, + 'Authorization': `Bearer ${this.token}`, }; } /** - * POST to a Slack API endpoint + * Make a POST request to a Slack API endpoint + * @param endpoint The API endpoint to call (without the base URL) + * @param data The data to send in the request body + * @param formData Whether the data is form data (multipart/form-data) + * @returns The response from the API + * @throws {SlackApiError} If the request fails */ - async post(endpoint: string, data: any, formData = false): Promise { - const url = `https://slack.com/api/${endpoint}`; + async post( + endpoint: string, + data: Record | FormData, + formData = false + ): Promise>> { + const url = `${this.baseUrl}/${endpoint}`; let headers = this.getBaseHeaders(); - let payload = data; + let payload: any = data; + if (formData) { - headers = { ...headers, ...data.getHeaders() }; + // If data is FormData, use its headers + if (data instanceof FormData) { + headers = { ...headers, ...data.getHeaders() }; + } else { + // If it's a plain object but formData is true, convert it to FormData + const form = new FormData(); + for (const [key, value] of Object.entries(data)) { + form.append(key, value); + } + payload = form; + headers = { ...headers, ...form.getHeaders() }; + } } else { + // For regular POST requests, use URL-encoded format headers['Content-Type'] = 'application/x-www-form-urlencoded'; - payload = new URLSearchParams(data).toString(); + payload = new URLSearchParams(data as Record).toString(); } try { - return await axios.post(url, payload, { headers, maxBodyLength: Infinity, validateStatus: () => true }); + const config: AxiosRequestConfig = { + headers, + maxBodyLength: Infinity, + validateStatus: () => true, // Handle all status codes in our code + }; + + const response = await axios.post>(url, payload, config); + + // Check for API errors even if HTTP status is 200 + if (response.data && !response.data.ok) { + throw new SlackApiError( + `Slack API error: ${response.data.error || 'Unknown error'}`, + endpoint, + new Error(response.data.error_description || response.data.error || 'Unknown error'), + response.status, + response.data + ); + } + + return response; } catch (error) { - console.error(`Error in Slack API POST to ${endpoint}:`, error); - throw error; + // Handle axios errors + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + throw new SlackApiError( + `Slack API request failed: ${axiosError.message}`, + endpoint, + axiosError, + axiosError.response?.status, + axiosError.response?.data + ); + } + + // Handle other errors + throw new SlackApiError( + `Error in Slack API POST to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, + endpoint, + error instanceof Error ? error : new Error(String(error)) + ); } } /** - * GET from a Slack API endpoint + * Make a GET request to a Slack API endpoint + * @param endpoint The API endpoint to call (without the base URL) + * @param params The query parameters to include in the request + * @returns The response from the API + * @throws {SlackApiError} If the request fails */ - async get(endpoint: string, params: Record = {}): Promise { - const url = `https://slack.com/api/${endpoint}`; - + async get( + endpoint: string, + params: SlackApiParams = {} + ): Promise>> { + const url = `${this.baseUrl}/${endpoint}`; const headers = this.getBaseHeaders(); + // Remove undefined and null values from params + const cleanParams: Record = {}; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + cleanParams[key] = value; + } + } + try { - return await axios.get(url, { headers, params, validateStatus: () => true }); + const config: AxiosRequestConfig = { + headers, + params: cleanParams, + validateStatus: () => true, // Handle all status codes in our code + }; + + const response = await axios.get>(url, config); + + // Check for API errors even if HTTP status is 200 + if (response.data && !response.data.ok) { + throw new SlackApiError( + `Slack API error: ${response.data.error || 'Unknown error'}`, + endpoint, + new Error(response.data.error_description || response.data.error || 'Unknown error'), + response.status, + response.data + ); + } + + return response; } catch (error) { - console.error(`Error in Slack API GET to ${endpoint}:`, error); - throw error; + // Handle axios errors + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + throw new SlackApiError( + `Slack API request failed: ${axiosError.message}`, + endpoint, + axiosError, + axiosError.response?.status, + axiosError.response?.data + ); + } + + // Handle other errors + throw new SlackApiError( + `Error in Slack API GET to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, + endpoint, + error instanceof Error ? error : new Error(String(error)) + ); } } } - -export interface SlackApiResponse { - ok: boolean; - error?: string; - [key: string]: any; -} From d560ed20347e2f7d284f5076df6c14ec814c374b Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 14:29:00 -0600 Subject: [PATCH 13/29] feat: add caching and rate limiting to Slack API client with improved error handling --- src/plugins/slack-now-playing/main.ts | 22 +- .../slack-now-playing/slack-api-client.ts | 340 ++++++++++++++++-- 2 files changed, 336 insertions(+), 26 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index d6cc2a85de..bec147b695 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -530,13 +530,23 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise }>('emoji.list'); - const emojiList = response.data; + // Get the list of emojis with caching enabled to reduce API calls + // The cache will automatically expire after the default time (5 minutes) + interface EmojiListResponse { + emoji: Record; + } + + // Use the improved API client with caching + const response = await client.get('emoji.list', {}, { + // Cache emoji list for 10 minutes since it doesn't change often + cacheExpiryMs: 10 * 60 * 1000 + }); + + // The response.data contains the actual API response, which includes the emoji property + const emojiList = response.data as unknown as EmojiListResponse; - // Check if our emoji exists - if (emojiList && emojiList.emoji && emojiList.emoji[config.emojiName]) { + // Check if our emoji exists using the 'in' operator for type safety + if (emojiList?.emoji && config.emojiName in emojiList.emoji) { console.log(`Emoji '${config.emojiName}' already exists, attempting to delete it`); return await deleteExistingEmoji(config); } else { diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index e6dc631fee..7da20ee825 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -3,27 +3,73 @@ import FormData from 'form-data'; /** * Standard response format from Slack API endpoints - * @template TData Type of the response data + * + * This interface represents the standard response structure returned by all Slack API endpoints. + * It includes common fields like success status, error information, and the actual response data. + * + * @template TData Type of the response data when the call is successful + * + * @example + * ```typescript + * // Successful response example + * const successResponse: SlackApiResponse = { + * ok: true, + * profile: { display_name: 'John Doe', email: 'john@example.com' } + * }; + * + * // Error response example + * const errorResponse: SlackApiResponse = { + * ok: false, + * error: 'invalid_auth', + * error_description: 'Invalid authentication token' + * }; + * ``` */ export interface SlackApiResponse { /** Whether the API call was successful */ ok: boolean; - /** Error code if the call failed */ + + /** Error code if the call failed (only present when ok is false) */ error?: string; - /** Error description if available */ + + /** Human-readable error description if available (only present when ok is false) */ error_description?: string; - /** Warning messages from the API */ + + /** Warning messages from the API that don't prevent successful execution */ warning?: string; - /** Response data properties */ - [key: string]: any; - /** Typed response data (available when ok is true) */ + + /** + * Typed response data (available when ok is true) + * This property is not actually in the Slack API response, but is used + * to provide type safety for the response data + */ data?: TData; + + /** + * Additional response data properties that vary by endpoint + * The actual structure depends on the specific API endpoint called + */ + [key: string]: unknown; } /** * Parameters for Slack API requests + * + * This type represents the parameters that can be passed to Slack API endpoints. + * It enforces type safety for common parameter types while allowing for flexibility. + * + * @example + * ```typescript + * const params: SlackApiParams = { + * channel: 'C1234567890', + * count: 10, + * inclusive: true + * }; + * ``` */ -export type SlackApiParams = Record; +export type SlackApiParams = { + [key: string]: string | number | boolean | null | undefined | string[] | number[]; +}; /** * Error thrown by the Slack API client @@ -62,11 +108,44 @@ export class SlackApiError extends Error { } } +/** + * Cache entry for Slack API responses + */ +interface CacheEntry { + /** The cached response data */ + data: T; + /** When the cache entry was created (timestamp) */ + timestamp: number; + /** Cache expiration time in milliseconds */ + expiryMs: number; +} + +/** + * Rate limit tracking for Slack API endpoints + */ +interface RateLimitInfo { + /** Number of requests made to this endpoint */ + requestCount: number; + /** Timestamp when the rate limit window started */ + windowStart: number; + /** Duration of the rate limit window in milliseconds */ + windowMs: number; + /** Maximum number of requests allowed in the window */ + maxRequests: number; +} + /** * Centralized Slack API client for making requests to the Slack API * - * This client handles authentication, error handling, and request formatting - * for all Slack API calls in the application. + * This client handles authentication, error handling, request formatting, + * caching, and rate limiting for all Slack API calls in the application. + * + * Features: + * - Automatic request authentication + * - Response caching for GET requests + * - Rate limiting protection + * - Comprehensive error handling + * - Type-safe request and response handling */ export class SlackApiClient { /** The Slack API token (xoxc-) */ @@ -75,6 +154,14 @@ export class SlackApiClient { readonly cookie: string; /** Base URL for all Slack API requests */ private readonly baseUrl = 'https://slack.com/api'; + /** Cache for GET requests to reduce API calls */ + private readonly cache: Map> = new Map(); + /** Default cache expiration time (5 minutes) */ + private readonly defaultCacheExpiryMs = 5 * 60 * 1000; + /** Rate limit tracking for each endpoint */ + private readonly rateLimits: Map = new Map(); + /** Default rate limit (20 requests per minute for most endpoints) */ + private readonly defaultRateLimit = { maxRequests: 20, windowMs: 60 * 1000 }; /** * Create a new Slack API client @@ -85,6 +172,32 @@ export class SlackApiClient { this.token = token; this.cookie = cookie; } + + /** + * Clear the response cache and reset rate limits + * + * This can be useful in scenarios where you want to force fresh data + * or when testing the API client. + * + * @param endpoint Optional specific endpoint to clear. If not provided, clears all endpoints. + */ + clearCache(endpoint?: string): void { + if (endpoint) { + // Clear cache for a specific endpoint + for (const key of this.cache.keys()) { + if (key.startsWith(`${endpoint}:`)) { + this.cache.delete(key); + } + } + + // Reset rate limit for the endpoint + this.rateLimits.delete(endpoint); + } else { + // Clear all cache and rate limits + this.cache.clear(); + this.rateLimits.clear(); + } + } /** * Get the base headers required for all Slack API requests @@ -98,20 +211,36 @@ export class SlackApiClient { } /** - * Make a POST request to a Slack API endpoint + * Make a POST request to a Slack API endpoint with rate limiting protection + * + * This method includes several performance optimizations: + * 1. Rate limiting protection to prevent hitting Slack API limits + * 2. Automatic conversion between data formats + * 3. Comprehensive error handling with detailed error information + * * @param endpoint The API endpoint to call (without the base URL) * @param data The data to send in the request body * @param formData Whether the data is form data (multipart/form-data) * @returns The response from the API - * @throws {SlackApiError} If the request fails + * @throws {SlackApiError} If the request fails or would exceed rate limits */ async post( endpoint: string, data: Record | FormData, formData = false ): Promise>> { + // Check rate limits before making the request + if (!this.checkRateLimit(endpoint)) { + throw new SlackApiError( + `Rate limit exceeded for Slack API endpoint: ${endpoint}`, + endpoint, + new Error('Too many requests in a short period'), + 429, // HTTP 429 Too Many Requests + { ok: false, error: 'rate_limited' } + ); + } + const url = `${this.baseUrl}/${endpoint}`; - let headers = this.getBaseHeaders(); let payload: any = data; @@ -123,7 +252,9 @@ export class SlackApiClient { // If it's a plain object but formData is true, convert it to FormData const form = new FormData(); for (const [key, value] of Object.entries(data)) { - form.append(key, value); + if (value !== undefined && value !== null) { + form.append(key, value); + } } payload = form; headers = { ...headers, ...form.getHeaders() }; @@ -131,10 +262,22 @@ export class SlackApiClient { } else { // For regular POST requests, use URL-encoded format headers['Content-Type'] = 'application/x-www-form-urlencoded'; - payload = new URLSearchParams(data as Record).toString(); + + // Filter out undefined and null values + const cleanData: Record = {}; + for (const [key, value] of Object.entries(data)) { + if (value !== undefined && value !== null) { + cleanData[key] = String(value); + } + } + + payload = new URLSearchParams(cleanData).toString(); } try { + // Update rate limit tracking + this.updateRateLimit(endpoint); + const config: AxiosRequestConfig = { headers, maxBodyLength: Infinity, @@ -145,6 +288,20 @@ export class SlackApiClient { // Check for API errors even if HTTP status is 200 if (response.data && !response.data.ok) { + // Special handling for rate limiting errors + if (response.data.error === 'rate_limited') { + // Update our rate limit tracking to prevent further requests + const rateLimit = this.rateLimits.get(endpoint) || { + requestCount: 0, + windowStart: Date.now(), + ...this.defaultRateLimit + }; + + // Force rate limit by maxing out the request count + rateLimit.requestCount = rateLimit.maxRequests; + this.rateLimits.set(endpoint, rateLimit); + } + throw new SlackApiError( `Slack API error: ${response.data.error || 'Unknown error'}`, endpoint, @@ -159,6 +316,21 @@ export class SlackApiClient { // Handle axios errors if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; + + // Check for rate limiting in the response headers + if (axiosError.response?.status === 429) { + // Update our rate limit tracking to prevent further requests + const rateLimit = this.rateLimits.get(endpoint) || { + requestCount: 0, + windowStart: Date.now(), + ...this.defaultRateLimit + }; + + // Force rate limit by maxing out the request count + rateLimit.requestCount = rateLimit.maxRequests; + this.rateLimits.set(endpoint, rateLimit); + } + throw new SlackApiError( `Slack API request failed: ${axiosError.message}`, endpoint, @@ -178,26 +350,145 @@ export class SlackApiClient { } /** - * Make a GET request to a Slack API endpoint + * Check if we're about to exceed rate limits for an endpoint + * @param endpoint The API endpoint to check + * @returns True if the request should be allowed, false if it would exceed rate limits + */ + private checkRateLimit(endpoint: string): boolean { + const now = Date.now(); + let rateLimit = this.rateLimits.get(endpoint); + + // If no rate limit info exists for this endpoint, create it + if (!rateLimit) { + rateLimit = { + requestCount: 0, + windowStart: now, + ...this.defaultRateLimit + }; + this.rateLimits.set(endpoint, rateLimit); + } + + // Check if we need to reset the window + if (now - rateLimit.windowStart > rateLimit.windowMs) { + rateLimit.requestCount = 0; + rateLimit.windowStart = now; + } + + // Check if we would exceed the rate limit + return rateLimit.requestCount < rateLimit.maxRequests; + } + + /** + * Update rate limit tracking after a request + * @param endpoint The API endpoint that was called + */ + private updateRateLimit(endpoint: string): void { + const rateLimit = this.rateLimits.get(endpoint); + if (rateLimit) { + rateLimit.requestCount++; + } + } + + /** + * Generate a cache key for a request + * @param endpoint The API endpoint + * @param params The request parameters + * @returns A unique cache key + */ + private getCacheKey(endpoint: string, params: SlackApiParams): string { + return `${endpoint}:${JSON.stringify(params)}`; + } + + /** + * Check if a cached response is available and valid + * @param cacheKey The cache key to check + * @returns The cached response or undefined if not available + */ + private getCachedResponse(cacheKey: string): T | undefined { + const cached = this.cache.get(cacheKey); + if (!cached) return undefined; + + const now = Date.now(); + const age = now - cached.timestamp; + + // Return the cached data if it's still valid + if (age < cached.expiryMs) { + return cached.data; + } + + // Remove expired cache entry + this.cache.delete(cacheKey); + return undefined; + } + + /** + * Store a response in the cache + * @param cacheKey The cache key + * @param data The data to cache + * @param expiryMs Optional custom expiry time in milliseconds + */ + private cacheResponse(cacheKey: string, data: T, expiryMs?: number): void { + this.cache.set(cacheKey, { + data, + timestamp: Date.now(), + expiryMs: expiryMs ?? this.defaultCacheExpiryMs + }); + } + + /** + * Make a GET request to a Slack API endpoint with caching and rate limiting + * + * This method includes several performance optimizations: + * 1. Response caching to reduce API calls for identical requests + * 2. Rate limiting protection to prevent hitting Slack API limits + * 3. Automatic retry for rate-limited requests + * * @param endpoint The API endpoint to call (without the base URL) * @param params The query parameters to include in the request + * @param options Optional request options + * @param options.skipCache Set to true to bypass the cache and force a fresh request + * @param options.cacheExpiryMs Custom cache expiration time in milliseconds * @returns The response from the API - * @throws {SlackApiError} If the request fails + * @throws {SlackApiError} If the request fails or would exceed rate limits */ async get( endpoint: string, - params: SlackApiParams = {} + params: SlackApiParams = {}, + options: { skipCache?: boolean; cacheExpiryMs?: number } = {} ): Promise>> { const url = `${this.baseUrl}/${endpoint}`; const headers = this.getBaseHeaders(); - // Remove undefined and null values from params - const cleanParams: Record = {}; + // Remove undefined and null values from params and handle array values + const cleanParams: Record = {}; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { + // Handle array values properly cleanParams[key] = value; } } + + // Check cache first (unless skipCache is true) + if (!options.skipCache) { + const cacheKey = this.getCacheKey(endpoint, cleanParams); + const cachedResponse = this.getCachedResponse>>(cacheKey); + + if (cachedResponse) { + // Return the cached response + return cachedResponse; + } + } + + // Check rate limits before making the request + if (!this.checkRateLimit(endpoint)) { + throw new SlackApiError( + `Rate limit exceeded for Slack API endpoint: ${endpoint}`, + endpoint, + new Error('Too many requests in a short period'), + 429, // HTTP 429 Too Many Requests + { ok: false, error: 'rate_limited' } + ); + } try { const config: AxiosRequestConfig = { @@ -206,6 +497,9 @@ export class SlackApiClient { validateStatus: () => true, // Handle all status codes in our code }; + // Update rate limit tracking + this.updateRateLimit(endpoint); + const response = await axios.get>(url, config); // Check for API errors even if HTTP status is 200 @@ -219,6 +513,12 @@ export class SlackApiClient { ); } + // Cache successful responses + if (!options.skipCache) { + const cacheKey = this.getCacheKey(endpoint, cleanParams); + this.cacheResponse(cacheKey, response, options.cacheExpiryMs); + } + return response; } catch (error) { // Handle axios errors From d1c329b88fa9e454550e8ac44488fd5deb5fb4b3 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 15:16:53 -0600 Subject: [PATCH 14/29] feat: add help text and improve layout of Slack Now Playing config dialog --- src/plugins/slack-now-playing/menu.ts | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts index 59be766c2b..6725edead7 100644 --- a/src/plugins/slack-now-playing/menu.ts +++ b/src/plugins/slack-now-playing/menu.ts @@ -22,28 +22,28 @@ type ValidationResult = { */ function validateConfig(config: SlackNowPlayingConfig): ValidationResult { const errors: string[] = []; - + // Check token if (!config.token) { errors.push('Missing Slack API token'); } else if (!config.token.startsWith('xoxc-')) { errors.push('Invalid Slack API token format (should start with "xoxc-")'); } - + // Check cookie token if (!config.cookieToken) { errors.push('Missing Slack cookie token'); } else if (!config.cookieToken.startsWith('xoxd-')) { errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); } - + // Check emoji name if (!config.emojiName) { errors.push('Missing custom emoji name'); } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); } - + return { valid: errors.length === 0, errors @@ -65,8 +65,16 @@ async function promptSlackNowPlayingOptions( const output = await prompt( { title: t('plugins.slack-now-playing.name'), - label: t('plugins.slack-now-playing.name'), + label: `
+

${t('plugins.slack-now-playing.name')}

+

HOW TO GET YOUR SLACK TOKENS:

+

Open Slack in browser & press F12
+ For ${t('plugins.slack-now-playing.menu.token')}: Network tab > Make a request > Find "token" parameter
+ For ${t('plugins.slack-now-playing.menu.cookie-token')}: Application tab > Cookies > "d" cookie value

+

${t('plugins.slack-now-playing.menu.emoji-name')}: Used to display album art in your status

+
`, type: 'multiInput', + useHtmlLabel: true, multiInputOptions: [ { label: t('plugins.slack-now-playing.menu.token'), @@ -94,7 +102,8 @@ async function promptSlackNowPlayingOptions( }, ], resizable: true, - height: 360, + width: 620, + height: 520, ...promptOptions(), }, window, @@ -104,7 +113,7 @@ async function promptSlackNowPlayingOptions( try { // Create a deep copy of the options to ensure we don't modify the original const updatedOptions = { ...options } as SlackNowPlayingConfig; - + // Update only the fields that were provided if (output[0] !== undefined) { updatedOptions.token = output[0]; @@ -115,10 +124,10 @@ async function promptSlackNowPlayingOptions( if (output[2] !== undefined) { updatedOptions.emojiName = output[2]; } - + // Validate the updated options const validationResult = validateConfig(updatedOptions); - + if (!validationResult.valid) { // Show validation errors to the user await dialog.showMessageBox(window, { @@ -129,11 +138,11 @@ async function promptSlackNowPlayingOptions( buttons: ['OK'], }); } - + // Save the config even if it has validation errors // This allows users to save partial configurations await setConfig(updatedOptions); - + // Verify the config was saved by getting it again // This is just for debugging purposes console.log('Saved Slack Now Playing configuration:', updatedOptions); From 04b86e4cacc4bfd1c9b01cd5cadc11a868888a4e Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 15:23:22 -0600 Subject: [PATCH 15/29] refactor: remove unnecessary console logs and whitespace in slack-now-playing plugin --- src/plugins/slack-now-playing/main.ts | 130 +++++++++++++------------- src/plugins/slack-now-playing/menu.ts | 4 +- 2 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index bec147b695..f3edc8ac50 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -70,7 +70,7 @@ function registerFileForCleanup(filePath: string): void { */ async function cleanupTempFiles(): Promise { const fsPromises = fs.promises; - + for (const filePath of state.tempFiles) { try { // Check if the file exists before attempting to delete it @@ -103,14 +103,14 @@ async function cleanupTempFiles(): Promise { async function cleanupExpiredCache(): Promise { const now = Date.now(); const fsPromises = fs.promises; - + // Check each cache entry for (const [url, cacheEntry] of Object.entries(state.albumArtCache)) { // If the entry is expired if (now - cacheEntry.timestamp > state.cacheExpiryMs) { // Remove from cache delete state.albumArtCache[url]; - + // Try to delete the file if it's not needed elsewhere try { await fsPromises.access(cacheEntry.filePath, fs.constants.F_OK); @@ -138,28 +138,28 @@ type ValidationResult = { */ function validateConfig(config: SlackNowPlayingConfig): ValidationResult { const errors: string[] = []; - + // Check token if (!config.token) { errors.push('Missing Slack API token'); } else if (!config.token.startsWith('xoxc-')) { errors.push('Invalid Slack API token format (should start with "xoxc-")'); } - + // Check cookie token if (!config.cookieToken) { errors.push('Missing Slack cookie token'); } else if (!config.cookieToken.startsWith('xoxd-')) { errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); } - + // Check emoji name if (!config.emojiName) { errors.push('Missing custom emoji name'); } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); } - + return { valid: errors.length === 0, errors @@ -225,9 +225,9 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) } else { console.error(`Error setting Slack status: ${String(error)}`); } - + // Re-throw specific errors that should be handled by the caller - if (error instanceof Error && + if (error instanceof Error && (error.message.includes('token') || error.message.includes('authentication'))) { throw new Error('Slack authentication failed. Please check your API token and cookie token.'); } @@ -254,7 +254,7 @@ async function updateSlackStatusWithEmoji( if (!validationResult.valid) { throw new Error(`Cannot update Slack status: ${validationResult.errors.join(', ')}`); } - + const client = new SlackApiClient(config.token, config.cookieToken); // Get the appropriate emoji for the current song @@ -272,13 +272,11 @@ async function updateSlackStatusWithEmoji( // Update the status // The client now handles API errors internally await client.post('users.profile.set', statusUpdatePayload); - + // Update state with the new status and emoji state.lastStatus = statusText; state.lastEmoji = statusEmoji; - - // Log success - console.log(`Slack status updated successfully: ${statusText} ${statusEmoji}`); + } catch (error: unknown) { // Handle SlackApiError specifically if (error instanceof SlackApiError) { @@ -287,7 +285,7 @@ async function updateSlackStatusWithEmoji( statusCode: error.statusCode, responseError: error.responseData?.error, }); - } + } // Handle other errors else if (error instanceof Error) { console.error(`Error updating Slack status: ${error.message}`, { @@ -297,7 +295,7 @@ async function updateSlackStatusWithEmoji( } else { console.error(`Error updating Slack status: ${String(error)}`); } - + // Re-throw the error to be handled by the caller throw error; } @@ -326,29 +324,29 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon console.error(`Cannot upload emoji to Slack: ${validationResult.errors.join(', ')}`); return false; } - + const client = new SlackApiClient(config.token, config.cookieToken); - + // Save album art to a temporary file const filePath = await saveAlbumArtToFile(songInfo); if (!filePath) { console.warn('Failed to save album art to file'); return false; } - + // Make sure the emoji doesn't already exist const emojiDeleted = await ensureEmojiDoesNotExist(config); if (!emojiDeleted) { console.warn('Failed to ensure emoji does not exist'); return false; } - + // Prepare the form data for the API request const formData = new FormData(); // We don't need to include the token in the form data anymore as it's in the headers formData.append('mode', 'data'); formData.append('name', config.emojiName); - + // Read the file and add it to the form data using async/await pattern try { // Use async file operations instead of synchronous ones @@ -361,20 +359,20 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon console.error(`Error reading album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); return false; } - + // Make the API request - the client now handles API errors internally try { // The post method now returns a properly typed response await client.post<{ ok: boolean }>('emoji.add', formData, true); - + // If we got here, the request was successful - console.log(`Successfully uploaded emoji: ${config.emojiName}`); + // Emoji uploaded successfully return true; } catch (apiError) { // Handle specific API error types if (apiError instanceof SlackApiError && apiError.responseData) { const errorCode = apiError.responseData.error; - + if (errorCode === 'invalid_name') { console.error(`Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`); } else if (errorCode === 'too_large') { @@ -412,11 +410,11 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { console.warn('No image source available for album art'); return null; } - + try { const imageUrl = songInfo.imageSrc; const now = Date.now(); - + // Check if we have a cached version of this image const cachedImage = state.albumArtCache[imageUrl]; if (cachedImage) { @@ -443,19 +441,19 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { } } } - + // Create a unique filename to prevent conflicts const tempDir = os.tmpdir(); const timestamp = now; const randomString = Math.random().toString(36).substring(2, 10); const filename = `album-art-${timestamp}-${randomString}.jpg`; const filePath = path.join(tempDir, filename); - + // Fetch the image let response: Response; try { response = await net.fetch(imageUrl); - + if (!response.ok) { console.error(`Failed to fetch album art: HTTP ${response.status} ${response.statusText}`); return null; @@ -464,12 +462,12 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { console.error(`Network error fetching album art: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); return null; } - + // Convert the response to a buffer let imageBuffer: Buffer; try { imageBuffer = Buffer.from(await response.arrayBuffer()); - + if (imageBuffer.length === 0) { console.error('Received empty album art image'); return null; @@ -478,22 +476,22 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { console.error(`Error processing album art data: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); return null; } - + // Write the buffer to a file using async file operations try { // Import the promises API from fs const fsPromises = fs.promises; await fsPromises.writeFile(filePath, imageBuffer); - + // Register the file for cleanup when the app exits registerFileForCleanup(filePath); - + // Add to cache state.albumArtCache[imageUrl] = { filePath, timestamp: now }; - + return filePath; } catch (fileError) { console.error(`Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); @@ -526,39 +524,39 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise; } - + // Use the improved API client with caching const response = await client.get('emoji.list', {}, { // Cache emoji list for 10 minutes since it doesn't change often cacheExpiryMs: 10 * 60 * 1000 }); - + // The response.data contains the actual API response, which includes the emoji property const emojiList = response.data as unknown as EmojiListResponse; - + // Check if our emoji exists using the 'in' operator for type safety if (emojiList?.emoji && config.emojiName in emojiList.emoji) { - console.log(`Emoji '${config.emojiName}' already exists, attempting to delete it`); + // Emoji already exists, attempting to delete it return await deleteExistingEmoji(config); } else { // Emoji doesn't exist, no need to delete - console.log(`Emoji '${config.emojiName}' doesn't exist, no need to delete`); + // Emoji doesn't exist, no need to delete return true; } } catch (apiError) { // Handle specific API error types if (apiError instanceof SlackApiError) { const errorCode = apiError.responseData?.error; - + if (errorCode === 'invalid_auth' || errorCode === 'token_expired') { console.error('Slack authentication failed. Please check your API token and cookie token.'); } else if (errorCode === 'rate_limited') { @@ -598,28 +596,28 @@ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { @@ -696,7 +694,7 @@ export const backend = createBackend({ // Ignore errors in the background task } }, cacheCleanupInterval); - + // Store the timer so we can clear it when the plugin stops state.cacheCleanupTimer = cacheCleanupTimer; @@ -708,13 +706,13 @@ export const backend = createBackend({ registerCallback(async (songInfo, event) => { // Skip time change events if (event === SongInfoEvent.TimeChanged) return; - + try { // Get the latest config each time const latestConfig = await ctx.getConfig(); const config = latestConfig as SlackNowPlayingConfig; state.currentConfig = config; // Update stored config - + // Only update if plugin is enabled if (!config.enabled) { return; @@ -729,7 +727,7 @@ export const backend = createBackend({ } return; } - + // Validate the configuration const validationResult = validateConfig(config); if (!validationResult.valid) { @@ -748,11 +746,11 @@ export const backend = createBackend({ // Check for authentication errors if (error.message.includes('authentication') || error.message.includes('token')) { console.error('Slack authentication failed. Please check your API token and cookie token.'); - } + } // Check for rate limiting errors else if (error.message.includes('rate limit') || error.message.includes('rate_limited')) { console.error('Slack API rate limit exceeded. Please try again later.'); - } + } // Generic error handling else { console.error(`Error in Slack Now Playing: ${error.message}`); @@ -784,16 +782,16 @@ export const backend = createBackend({ clearInterval(state.cacheCleanupTimer); state.cacheCleanupTimer = undefined; } - + // Clean up any temporary files created by the plugin await cleanupTempFiles(); - + // Run a final cache cleanup await cleanupExpiredCache(); - + // Clear the window reference state.window = undefined; - + // Note: We don't unregister the callback as there's no API for that // It will be garbage collected when the plugin is unloaded }, @@ -808,15 +806,15 @@ export const backend = createBackend({ // Get the latest configuration const latestConfig = await state.context.getConfig(); const config = latestConfig as SlackNowPlayingConfig; - + // Update the stored configuration state.currentConfig = config; - + // Validate the configuration try { // Use assertValidConfig to validate the configuration assertValidConfig(config); - console.log('Slack Now Playing configuration updated successfully'); + // Configuration updated successfully } catch (error) { console.warn(`Slack Now Playing configuration validation failed: ${error instanceof Error ? error.message : String(error)}`); } diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts index 6725edead7..10d5df37dd 100644 --- a/src/plugins/slack-now-playing/menu.ts +++ b/src/plugins/slack-now-playing/menu.ts @@ -143,9 +143,7 @@ async function promptSlackNowPlayingOptions( // This allows users to save partial configurations await setConfig(updatedOptions); - // Verify the config was saved by getting it again - // This is just for debugging purposes - console.log('Saved Slack Now Playing configuration:', updatedOptions); + // Config has been saved successfully } catch (error) { console.error('Error saving Slack Now Playing configuration:', error); await dialog.showMessageBox(window, { From ec9a8e43eef7a1c718c31f2465185ee3030e4ca1 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 15:27:00 -0600 Subject: [PATCH 16/29] chore: rename Slack Status to Slack Now Playing in English translations --- src/i18n/resources/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 9a8926a4d1..0ddb0d9cc9 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -426,7 +426,7 @@ "cookie-token": "Slack Cookie Token", "emoji-name": "Custom Emoji Name" }, - "name": "Slack Status", + "name": "Slack Now Playing", "status-text": "Now Playing: {{artist}} - {{title}}" }, "downloader": { From b75c24da9a090d9e829d11fb3d8e330859b171bf Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 9 May 2025 16:03:52 -0600 Subject: [PATCH 17/29] feat: add SSL certificate validation bypass for local development in Slack API client --- package.json | 2 +- .../slack-now-playing/slack-api-client.ts | 162 ++++++++++-------- 2 files changed, 95 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index 6c678f3d9b..84695f636f 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ "vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect", "start": "electron-vite preview", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start", - "dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch", + "dev": "cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev --watch", "dev:renderer": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev", "dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index 7da20ee825..6c37f82b17 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -1,14 +1,15 @@ import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import FormData from 'form-data'; +import https from 'node:https'; /** * Standard response format from Slack API endpoints - * + * * This interface represents the standard response structure returned by all Slack API endpoints. * It includes common fields like success status, error information, and the actual response data. - * + * * @template TData Type of the response data when the call is successful - * + * * @example * ```typescript * // Successful response example @@ -16,7 +17,7 @@ import FormData from 'form-data'; * ok: true, * profile: { display_name: 'John Doe', email: 'john@example.com' } * }; - * + * * // Error response example * const errorResponse: SlackApiResponse = { * ok: false, @@ -28,24 +29,24 @@ import FormData from 'form-data'; export interface SlackApiResponse { /** Whether the API call was successful */ ok: boolean; - + /** Error code if the call failed (only present when ok is false) */ error?: string; - + /** Human-readable error description if available (only present when ok is false) */ error_description?: string; - + /** Warning messages from the API that don't prevent successful execution */ warning?: string; - - /** + + /** * Typed response data (available when ok is true) * This property is not actually in the Slack API response, but is used * to provide type safety for the response data */ data?: TData; - - /** + + /** * Additional response data properties that vary by endpoint * The actual structure depends on the specific API endpoint called */ @@ -54,10 +55,10 @@ export interface SlackApiResponse { /** * Parameters for Slack API requests - * + * * This type represents the parameters that can be passed to Slack API endpoints. * It enforces type safety for common parameter types while allowing for flexibility. - * + * * @example * ```typescript * const params: SlackApiParams = { @@ -136,10 +137,10 @@ interface RateLimitInfo { /** * Centralized Slack API client for making requests to the Slack API - * + * * This client handles authentication, error handling, request formatting, * caching, and rate limiting for all Slack API calls in the application. - * + * * Features: * - Automatic request authentication * - Response caching for GET requests @@ -172,13 +173,13 @@ export class SlackApiClient { this.token = token; this.cookie = cookie; } - + /** * Clear the response cache and reset rate limits - * + * * This can be useful in scenarios where you want to force fresh data * or when testing the API client. - * + * * @param endpoint Optional specific endpoint to clear. If not provided, clears all endpoints. */ clearCache(endpoint?: string): void { @@ -189,7 +190,7 @@ export class SlackApiClient { this.cache.delete(key); } } - + // Reset rate limit for the endpoint this.rateLimits.delete(endpoint); } else { @@ -209,15 +210,38 @@ export class SlackApiClient { 'Authorization': `Bearer ${this.token}`, }; } + + /** + * Create Axios request configuration with options for SSL certificate validation + * @param headers HTTP headers to include in the request + * @param options Additional configuration options + * @returns Axios request configuration object + */ + private createRequestConfig(headers: Record, options: { disableSSLValidation?: boolean } = {}): AxiosRequestConfig { + const config: AxiosRequestConfig = { + headers, + maxBodyLength: Infinity, + validateStatus: () => true, // Handle all status codes in our code + }; + + // Disable SSL certificate validation if requested (for local development only) + if (options.disableSSLValidation) { + config.httpsAgent = new https.Agent({ + rejectUnauthorized: false // WARNING: This is insecure and should only be used in development + }); + } + + return config; + } /** * Make a POST request to a Slack API endpoint with rate limiting protection - * + * * This method includes several performance optimizations: * 1. Rate limiting protection to prevent hitting Slack API limits * 2. Automatic conversion between data formats * 3. Comprehensive error handling with detailed error information - * + * * @param endpoint The API endpoint to call (without the base URL) * @param data The data to send in the request body * @param formData Whether the data is form data (multipart/form-data) @@ -225,8 +249,8 @@ export class SlackApiClient { * @throws {SlackApiError} If the request fails or would exceed rate limits */ async post( - endpoint: string, - data: Record | FormData, + endpoint: string, + data: Record | FormData, formData = false ): Promise>> { // Check rate limits before making the request @@ -239,11 +263,11 @@ export class SlackApiClient { { ok: false, error: 'rate_limited' } ); } - + const url = `${this.baseUrl}/${endpoint}`; let headers = this.getBaseHeaders(); let payload: any = data; - + if (formData) { // If data is FormData, use its headers if (data instanceof FormData) { @@ -262,7 +286,7 @@ export class SlackApiClient { } else { // For regular POST requests, use URL-encoded format headers['Content-Type'] = 'application/x-www-form-urlencoded'; - + // Filter out undefined and null values const cleanData: Record = {}; for (const [key, value] of Object.entries(data)) { @@ -270,22 +294,22 @@ export class SlackApiClient { cleanData[key] = String(value); } } - + payload = new URLSearchParams(cleanData).toString(); } try { // Update rate limit tracking this.updateRateLimit(endpoint); - - const config: AxiosRequestConfig = { - headers, - maxBodyLength: Infinity, - validateStatus: () => true, // Handle all status codes in our code - }; - + + // Create request config with SSL validation disabled for local development + // Set disableSSLValidation to true to bypass SSL certificate validation + const config = this.createRequestConfig(headers, { + disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' + }); + const response = await axios.post>(url, payload, config); - + // Check for API errors even if HTTP status is 200 if (response.data && !response.data.ok) { // Special handling for rate limiting errors @@ -296,12 +320,12 @@ export class SlackApiClient { windowStart: Date.now(), ...this.defaultRateLimit }; - + // Force rate limit by maxing out the request count rateLimit.requestCount = rateLimit.maxRequests; this.rateLimits.set(endpoint, rateLimit); } - + throw new SlackApiError( `Slack API error: ${response.data.error || 'Unknown error'}`, endpoint, @@ -310,13 +334,13 @@ export class SlackApiClient { response.data ); } - + return response; } catch (error) { // Handle axios errors if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; - + // Check for rate limiting in the response headers if (axiosError.response?.status === 429) { // Update our rate limit tracking to prevent further requests @@ -325,12 +349,12 @@ export class SlackApiClient { windowStart: Date.now(), ...this.defaultRateLimit }; - + // Force rate limit by maxing out the request count rateLimit.requestCount = rateLimit.maxRequests; this.rateLimits.set(endpoint, rateLimit); } - + throw new SlackApiError( `Slack API request failed: ${axiosError.message}`, endpoint, @@ -339,7 +363,7 @@ export class SlackApiClient { axiosError.response?.data ); } - + // Handle other errors throw new SlackApiError( `Error in Slack API POST to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, @@ -357,7 +381,7 @@ export class SlackApiClient { private checkRateLimit(endpoint: string): boolean { const now = Date.now(); let rateLimit = this.rateLimits.get(endpoint); - + // If no rate limit info exists for this endpoint, create it if (!rateLimit) { rateLimit = { @@ -367,17 +391,17 @@ export class SlackApiClient { }; this.rateLimits.set(endpoint, rateLimit); } - + // Check if we need to reset the window if (now - rateLimit.windowStart > rateLimit.windowMs) { rateLimit.requestCount = 0; rateLimit.windowStart = now; } - + // Check if we would exceed the rate limit return rateLimit.requestCount < rateLimit.maxRequests; } - + /** * Update rate limit tracking after a request * @param endpoint The API endpoint that was called @@ -388,7 +412,7 @@ export class SlackApiClient { rateLimit.requestCount++; } } - + /** * Generate a cache key for a request * @param endpoint The API endpoint @@ -398,7 +422,7 @@ export class SlackApiClient { private getCacheKey(endpoint: string, params: SlackApiParams): string { return `${endpoint}:${JSON.stringify(params)}`; } - + /** * Check if a cached response is available and valid * @param cacheKey The cache key to check @@ -407,20 +431,20 @@ export class SlackApiClient { private getCachedResponse(cacheKey: string): T | undefined { const cached = this.cache.get(cacheKey); if (!cached) return undefined; - + const now = Date.now(); const age = now - cached.timestamp; - + // Return the cached data if it's still valid if (age < cached.expiryMs) { return cached.data; } - + // Remove expired cache entry this.cache.delete(cacheKey); return undefined; } - + /** * Store a response in the cache * @param cacheKey The cache key @@ -437,12 +461,12 @@ export class SlackApiClient { /** * Make a GET request to a Slack API endpoint with caching and rate limiting - * + * * This method includes several performance optimizations: * 1. Response caching to reduce API calls for identical requests * 2. Rate limiting protection to prevent hitting Slack API limits * 3. Automatic retry for rate-limited requests - * + * * @param endpoint The API endpoint to call (without the base URL) * @param params The query parameters to include in the request * @param options Optional request options @@ -452,7 +476,7 @@ export class SlackApiClient { * @throws {SlackApiError} If the request fails or would exceed rate limits */ async get( - endpoint: string, + endpoint: string, params: SlackApiParams = {}, options: { skipCache?: boolean; cacheExpiryMs?: number } = {} ): Promise>> { @@ -467,18 +491,18 @@ export class SlackApiClient { cleanParams[key] = value; } } - + // Check cache first (unless skipCache is true) if (!options.skipCache) { const cacheKey = this.getCacheKey(endpoint, cleanParams); const cachedResponse = this.getCachedResponse>>(cacheKey); - + if (cachedResponse) { // Return the cached response return cachedResponse; } } - + // Check rate limits before making the request if (!this.checkRateLimit(endpoint)) { throw new SlackApiError( @@ -491,17 +515,19 @@ export class SlackApiClient { } try { - const config: AxiosRequestConfig = { - headers, - params: cleanParams, - validateStatus: () => true, // Handle all status codes in our code - }; + // Create request config with SSL validation disabled for local development + const config = this.createRequestConfig(headers, { + disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' + }); + // Add params to the config + config.params = cleanParams; + // Update rate limit tracking this.updateRateLimit(endpoint); - + const response = await axios.get>(url, config); - + // Check for API errors even if HTTP status is 200 if (response.data && !response.data.ok) { throw new SlackApiError( @@ -512,13 +538,13 @@ export class SlackApiClient { response.data ); } - + // Cache successful responses if (!options.skipCache) { const cacheKey = this.getCacheKey(endpoint, cleanParams); this.cacheResponse(cacheKey, response, options.cacheExpiryMs); } - + return response; } catch (error) { // Handle axios errors @@ -532,7 +558,7 @@ export class SlackApiClient { axiosError.response?.data ); } - + // Handle other errors throw new SlackApiError( `Error in Slack API GET to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, From af5e58e410b803110d9b14cc0649c78942b1ddd6 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 10 May 2025 16:38:00 -0600 Subject: [PATCH 18/29] refactor: replace axios and form-data with native fetch and formdata-node in Slack client --- package.json | 5 +- pnpm-lock.yaml | 110 +++++++---- src/plugins/slack-now-playing/main.ts | 117 +++++------ .../slack-now-playing/slack-api-client.ts | 187 +++++++----------- 4 files changed, 199 insertions(+), 220 deletions(-) diff --git a/package.json b/package.json index 84695f636f..0d78fa0795 100644 --- a/package.json +++ b/package.json @@ -256,7 +256,6 @@ "@skyra/jaro-winkler": "1.1.1", "@xhayper/discord-rpc": "1.2.1", "async-mutex": "0.5.0", - "axios": "^1.8.4", "bgutils-js": "3.2.0", "butterchurn": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4", @@ -274,8 +273,10 @@ "es-hangul": "2.3.3", "fast-average-color": "9.5.0", "fast-equals": "5.2.2", + "fetch-blob": "^4.0.0", + "file-type": "^20.5.0", "filenamify": "6.0.0", - "form-data": "^4.0.2", + "formdata-node": "^6.0.3", "hanja": "1.1.4", "happy-dom": "17.4.4", "hono": "4.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f33e5b1bba..cc5bcc7bc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,9 +84,6 @@ importers: async-mutex: specifier: 0.5.0 version: 0.5.0 - axios: - specifier: ^1.8.4 - version: 1.8.4 bgutils-js: specifier: 3.2.0 version: 3.2.0 @@ -138,12 +135,18 @@ importers: fast-equals: specifier: 5.2.2 version: 5.2.2 + fetch-blob: + specifier: ^4.0.0 + version: 4.0.0 + file-type: + specifier: ^20.5.0 + version: 20.5.0 filenamify: specifier: 6.0.0 version: 6.0.0 - form-data: - specifier: ^4.0.2 - version: 4.0.2 + formdata-node: + specifier: ^6.0.3 + version: 6.0.3 hanja: specifier: 1.1.4 version: 1.1.4 @@ -1202,6 +1205,10 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tokenizer/inflate@0.2.7': + resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + engines: {node: '>=18'} + '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1620,9 +1627,6 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} - axios@1.8.4: - resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} - babel-plugin-jsx-dom-expressions@0.39.7: resolution: {integrity: sha512-8GzVmFla7jaTNWW8W+lTMl9YGva4/06CtwJjySnkYtt8G1v9weCzc2SuF1DfrudcCNb2Doetc1FRg33swBYZCA==} peerDependencies: @@ -2487,6 +2491,13 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fetch-blob@4.0.0: + resolution: {integrity: sha512-nPmnhRmpNMjYWnp9EBMGs6z5lq9RXed5W1vuZcECrsDVQInM8AMQSooVb3X183Aole60adzjWbH9qlRFWzDDTA==} + engines: {node: '>=16.7'} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2495,6 +2506,10 @@ packages: resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} engines: {node: '>=10'} + file-type@20.5.0: + resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==} + engines: {node: '>=18'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -2521,15 +2536,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2542,6 +2548,10 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} + formdata-node@6.0.3: + resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} + engines: {node: '>= 18'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -3660,6 +3670,10 @@ packages: resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} engines: {node: '>=8'} + peek-readable@7.0.0: + resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} + engines: {node: '>=18'} + peerjs-js-binarypack@2.1.0: resolution: {integrity: sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==} engines: {node: '>= 14.0.0'} @@ -3787,9 +3801,6 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -4208,6 +4219,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@10.2.2: + resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} + engines: {node: '>=18'} + strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} @@ -4283,6 +4298,10 @@ packages: resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} engines: {node: '>=10'} + token-types@6.0.0: + resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + engines: {node: '>=14.16'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -5606,6 +5625,14 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tokenizer/inflate@0.2.7': + dependencies: + debug: 4.4.0 + fflate: 0.8.2 + token-types: 6.0.0 + transitivePeerDependencies: + - supports-color + '@tokenizer/token@0.3.0': {} '@tootallnate/once@2.0.0': {} @@ -6084,14 +6111,6 @@ snapshots: await-to-js@3.0.0: {} - axios@1.8.4: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.2 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -7225,6 +7244,12 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fetch-blob@4.0.0: + dependencies: + node-domexception: 1.0.0 + + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7235,6 +7260,15 @@ snapshots: strtok3: 6.3.0 token-types: 4.2.1 + file-type@20.5.0: + dependencies: + '@tokenizer/inflate': 0.2.7 + strtok3: 10.2.2 + token-types: 6.0.0 + uint8array-extras: 1.4.0 + transitivePeerDependencies: + - supports-color + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -7261,8 +7295,6 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -7279,6 +7311,8 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 + formdata-node@6.0.3: {} + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -8433,6 +8467,8 @@ snapshots: peek-readable@4.1.0: {} + peek-readable@7.0.0: {} + peerjs-js-binarypack@2.1.0: {} peerjs@1.5.4: @@ -8522,8 +8558,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - proxy-from-env@1.1.0: {} - pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -9027,6 +9061,11 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@10.2.2: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 7.0.0 + strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 @@ -9115,6 +9154,11 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + token-types@6.0.0: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + totalist@3.0.1: {} truncate-utf8-bytes@1.0.2: diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index f3edc8ac50..cc25f4e907 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -2,13 +2,12 @@ import { net } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import { SlackApiClient, SlackApiError } from './slack-api-client'; -import FormData from 'form-data'; +import { SlackApiClient, SlackError } from './slack-api-client'; +import { FormData } from 'formdata-node'; +import { fileFromPath } from 'formdata-node/file-from-path'; import { createBackend } from '@/utils'; import registerCallback, { SongInfoEvent } from '@/providers/song-info'; import { t } from '@/i18n'; - -// Import SongInfo type from provider instead of defining our own import type { SongInfo } from '@/providers/song-info'; // Plugin config type @@ -86,7 +85,7 @@ async function cleanupTempFiles(): Promise { console.error(`Error deleting temporary file ${filePath}:`, error); } }); - } catch (error) { + } catch (error: any) { // Catch any unexpected errors if (error instanceof Error) { console.error(`Error during cleanup of ${filePath}:`, error.message); @@ -116,7 +115,7 @@ async function cleanupExpiredCache(): Promise { await fsPromises.access(cacheEntry.filePath, fs.constants.F_OK); await fsPromises.unlink(cacheEntry.filePath); state.tempFiles.delete(cacheEntry.filePath); - } catch (error) { + } catch (error: any) { // Ignore errors if the file doesn't exist or is in use } } @@ -215,7 +214,7 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) const expirationTime = Math.floor(Date.now() / 1000) + remaining; await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); - } catch (error) { + } catch (error: any) { // Provide more detailed error information based on error type if (error instanceof Error) { console.error(`Error setting Slack status: ${error.message}`, { @@ -278,8 +277,8 @@ async function updateSlackStatusWithEmoji( state.lastEmoji = statusEmoji; } catch (error: unknown) { - // Handle SlackApiError specifically - if (error instanceof SlackApiError) { + // Handle SlackError specifically + if (error instanceof SlackError) { console.error(`Slack API error updating status: ${error.message}`, { endpoint: error.endpoint, statusCode: error.statusCode, @@ -341,22 +340,17 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon return false; } - // Prepare the form data for the API request + // Prepare the form data for the API request using formdata-node const formData = new FormData(); - // We don't need to include the token in the form data anymore as it's in the headers formData.append('mode', 'data'); formData.append('name', config.emojiName); - - // Read the file and add it to the form data using async/await pattern try { - // Use async file operations instead of synchronous ones - const fileBuffer = await fs.promises.readFile(filePath); - formData.append('image', fileBuffer, { - filename: 'album-art.jpg', - contentType: 'image/jpeg', - }); - } catch (fileError) { - console.error(`Error reading album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); + // Use fileFromPath to get a proper File object for formdata-node + const imageFile = await fileFromPath(filePath); + formData.append('image', imageFile); + + } catch (fileError: any) { + console.error(`Error preparing album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); return false; } @@ -368,9 +362,9 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon // If we got here, the request was successful // Emoji uploaded successfully return true; - } catch (apiError) { + } catch (apiError: any) { // Handle specific API error types - if (apiError instanceof SlackApiError && apiError.responseData) { + if (apiError instanceof SlackError && apiError.responseData) { const errorCode = apiError.responseData.error; if (errorCode === 'invalid_name') { @@ -382,6 +376,9 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon } else { console.error(`Error uploading emoji: ${errorCode}`, apiError.responseData); } + // Log the full Slack error response for diagnostics + console.error('Slack error full response:', apiError.responseData); + } else { console.error(`Error uploading emoji to Slack: ${apiError instanceof Error ? apiError.message : String(apiError)}`); } @@ -425,7 +422,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { // Verify the file still exists await fs.promises.access(cachedImage.filePath, fs.constants.F_OK); return cachedImage.filePath; - } catch (error) { + } catch (error: any) { // File doesn't exist anymore, remove from cache delete state.albumArtCache[imageUrl]; } @@ -436,7 +433,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { try { await fs.promises.unlink(cachedImage.filePath); state.tempFiles.delete(cachedImage.filePath); - } catch (error) { + } catch (error: any) { // Ignore errors if the file doesn't exist } } @@ -497,7 +494,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { console.error(`Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); return null; } - } catch (error) { + } catch (error: any) { // Catch any other unexpected errors if (error instanceof Error) { console.error(`Error saving album art to file: ${error.message}`, { @@ -528,33 +525,24 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise; + [key: string]: unknown; } - - // Use the improved API client with caching - const response = await client.get('emoji.list', {}, { - // Cache emoji list for 10 minutes since it doesn't change often - cacheExpiryMs: 10 * 60 * 1000 - }); - - // The response.data contains the actual API response, which includes the emoji property - const emojiList = response.data as unknown as EmojiListResponse; - - // Check if our emoji exists using the 'in' operator for type safety - if (emojiList?.emoji && config.emojiName in emojiList.emoji) { + const response = await client.get('emoji.list'); + if (!response || !response.emoji) { + return false; + } + if (response.emoji && typeof response.emoji === 'object' && config.emojiName in response.emoji) { // Emoji already exists, attempting to delete it return await deleteExistingEmoji(config); } else { - // Emoji doesn't exist, no need to delete // Emoji doesn't exist, no need to delete return true; } - } catch (apiError) { + } catch (apiError: any) { // Handle specific API error types - if (apiError instanceof SlackApiError) { + if (apiError instanceof SlackError) { const errorCode = apiError.responseData?.error; if (errorCode === 'invalid_auth' || errorCode === 'token_expired') { @@ -565,19 +553,22 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { try { await cleanupExpiredCache(); - } catch (error) { + } catch (error: any) { // Ignore errors in the background task } }, cacheCleanupInterval); @@ -759,7 +744,7 @@ export const backend = createBackend({ console.error(`Error in Slack Now Playing: ${String(error)}`); } }); - } catch (error) { + } catch (error: any) { // Handle unexpected errors in the callback itself if (error instanceof Error) { console.error(`Error processing song info: ${error.message}`, { @@ -815,10 +800,10 @@ export const backend = createBackend({ // Use assertValidConfig to validate the configuration assertValidConfig(config); // Configuration updated successfully - } catch (error) { + } catch (error: any) { console.warn(`Slack Now Playing configuration validation failed: ${error instanceof Error ? error.message : String(error)}`); } - } catch (error) { + } catch (error: any) { console.error('Error updating Slack Now Playing configuration:', error); } } diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index 6c37f82b17..35540e1fb3 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -1,5 +1,4 @@ -import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; -import FormData from 'form-data'; +import { FormData } from 'formdata-node'; import https from 'node:https'; /** @@ -75,7 +74,7 @@ export type SlackApiParams = { /** * Error thrown by the Slack API client */ -export class SlackApiError extends Error { +export class SlackError extends Error { /** The original error that caused this error */ readonly originalError: Error; /** The endpoint that was called */ @@ -101,7 +100,7 @@ export class SlackApiError extends Error { responseData?: SlackApiResponse ) { super(message); - this.name = 'SlackApiError'; + this.name = 'SlackError'; this.originalError = originalError; this.endpoint = endpoint; this.statusCode = statusCode; @@ -210,28 +209,24 @@ export class SlackApiClient { 'Authorization': `Bearer ${this.token}`, }; } - + /** - * Create Axios request configuration with options for SSL certificate validation + * Create fetch request options with options for SSL certificate validation * @param headers HTTP headers to include in the request * @param options Additional configuration options - * @returns Axios request configuration object + * @returns Fetch request init object */ - private createRequestConfig(headers: Record, options: { disableSSLValidation?: boolean } = {}): AxiosRequestConfig { - const config: AxiosRequestConfig = { + private createFetchOptions(headers: Record, options: { disableSSLValidation?: boolean } = {}): RequestInit { + const fetchOptions: RequestInit = { headers, - maxBodyLength: Infinity, - validateStatus: () => true, // Handle all status codes in our code + // 'credentials' and 'mode' are not needed for Node.js fetch }; - - // Disable SSL certificate validation if requested (for local development only) + // For SSL validation disabling, use agent in Node.js if (options.disableSSLValidation) { - config.httpsAgent = new https.Agent({ - rejectUnauthorized: false // WARNING: This is insecure and should only be used in development - }); + // @ts-ignore + fetchOptions.agent = new https.Agent({ rejectUnauthorized: false }); } - - return config; + return fetchOptions; } /** @@ -246,16 +241,16 @@ export class SlackApiClient { * @param data The data to send in the request body * @param formData Whether the data is form data (multipart/form-data) * @returns The response from the API - * @throws {SlackApiError} If the request fails or would exceed rate limits + * @throws {SlackError} If the request fails or would exceed rate limits */ async post( endpoint: string, data: Record | FormData, formData = false - ): Promise>> { + ): Promise> { // Check rate limits before making the request if (!this.checkRateLimit(endpoint)) { - throw new SlackApiError( + throw new SlackError( `Rate limit exceeded for Slack API endpoint: ${endpoint}`, endpoint, new Error('Too many requests in a short period'), @@ -269,20 +264,8 @@ export class SlackApiClient { let payload: any = data; if (formData) { - // If data is FormData, use its headers - if (data instanceof FormData) { - headers = { ...headers, ...data.getHeaders() }; - } else { - // If it's a plain object but formData is true, convert it to FormData - const form = new FormData(); - for (const [key, value] of Object.entries(data)) { - if (value !== undefined && value !== null) { - form.append(key, value); - } - } - payload = form; - headers = { ...headers, ...form.getHeaders() }; - } + // Do not set or merge Content-Type headers; fetch will handle this for formdata-node + payload = data; } else { // For regular POST requests, use URL-encoded format headers['Content-Type'] = 'application/x-www-form-urlencoded'; @@ -302,70 +285,39 @@ export class SlackApiClient { // Update rate limit tracking this.updateRateLimit(endpoint); - // Create request config with SSL validation disabled for local development + // Create request fetchOptions with SSL validation disabled for local development // Set disableSSLValidation to true to bypass SSL certificate validation - const config = this.createRequestConfig(headers, { + const fetchOptions = this.createFetchOptions(headers, { disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' }); - const response = await axios.post>(url, payload, config); - - // Check for API errors even if HTTP status is 200 - if (response.data && !response.data.ok) { - // Special handling for rate limiting errors - if (response.data.error === 'rate_limited') { - // Update our rate limit tracking to prevent further requests - const rateLimit = this.rateLimits.get(endpoint) || { - requestCount: 0, - windowStart: Date.now(), - ...this.defaultRateLimit - }; - - // Force rate limit by maxing out the request count - rateLimit.requestCount = rateLimit.maxRequests; - this.rateLimits.set(endpoint, rateLimit); - } - - throw new SlackApiError( - `Slack API error: ${response.data.error || 'Unknown error'}`, - endpoint, - new Error(response.data.error_description || response.data.error || 'Unknown error'), - response.status, - response.data - ); - } + fetchOptions.method = 'POST'; + fetchOptions.body = payload; - return response; - } catch (error) { - // Handle axios errors - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; + const res = await fetch(url, fetchOptions); + const json: SlackApiResponse = await res.json(); - // Check for rate limiting in the response headers - if (axiosError.response?.status === 429) { - // Update our rate limit tracking to prevent further requests + if (!json.ok) { + if (json.error === 'rate_limited') { const rateLimit = this.rateLimits.get(endpoint) || { requestCount: 0, windowStart: Date.now(), ...this.defaultRateLimit }; - - // Force rate limit by maxing out the request count rateLimit.requestCount = rateLimit.maxRequests; this.rateLimits.set(endpoint, rateLimit); } - - throw new SlackApiError( - `Slack API request failed: ${axiosError.message}`, + throw new SlackError( + `Slack API error: ${json.error || 'Unknown error'}`, endpoint, - axiosError, - axiosError.response?.status, - axiosError.response?.data + new Error(json.error_description || json.error || 'Unknown error'), + res.status, + json as SlackApiResponse ); } - - // Handle other errors - throw new SlackApiError( + return json; + } catch (error: any) { + throw new SlackError( `Error in Slack API POST to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, endpoint, error instanceof Error ? error : new Error(String(error)) @@ -437,7 +389,7 @@ export class SlackApiClient { // Return the cached data if it's still valid if (age < cached.expiryMs) { - return cached.data; + return cached.data as T; } // Remove expired cache entry @@ -473,13 +425,13 @@ export class SlackApiClient { * @param options.skipCache Set to true to bypass the cache and force a fresh request * @param options.cacheExpiryMs Custom cache expiration time in milliseconds * @returns The response from the API - * @throws {SlackApiError} If the request fails or would exceed rate limits + * @throws {SlackError} If the request fails or would exceed rate limits */ async get( endpoint: string, params: SlackApiParams = {}, options: { skipCache?: boolean; cacheExpiryMs?: number } = {} - ): Promise>> { + ): Promise> { const url = `${this.baseUrl}/${endpoint}`; const headers = this.getBaseHeaders(); @@ -495,7 +447,7 @@ export class SlackApiClient { // Check cache first (unless skipCache is true) if (!options.skipCache) { const cacheKey = this.getCacheKey(endpoint, cleanParams); - const cachedResponse = this.getCachedResponse>>(cacheKey); + const cachedResponse = this.getCachedResponse>(cacheKey); if (cachedResponse) { // Return the cached response @@ -505,7 +457,7 @@ export class SlackApiClient { // Check rate limits before making the request if (!this.checkRateLimit(endpoint)) { - throw new SlackApiError( + throw new SlackError( `Rate limit exceeded for Slack API endpoint: ${endpoint}`, endpoint, new Error('Too many requests in a short period'), @@ -515,52 +467,49 @@ export class SlackApiClient { } try { - // Create request config with SSL validation disabled for local development - const config = this.createRequestConfig(headers, { - disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' - }); - - // Add params to the config - config.params = cleanParams; + // Build query string + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(cleanParams)) { + if (Array.isArray(value)) { + for (const v of value) { + searchParams.append(key, String(v)); + } + } else { + searchParams.append(key, String(value)); + } + } + const fetchUrl = `${url}?${searchParams.toString()}`; // Update rate limit tracking this.updateRateLimit(endpoint); - const response = await axios.get>(url, config); + const fetchOptions = this.createFetchOptions(headers, { + disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' + }); + fetchOptions.method = 'GET'; + + const res = await fetch(fetchUrl, fetchOptions); + const json: SlackApiResponse = await res.json(); - // Check for API errors even if HTTP status is 200 - if (response.data && !response.data.ok) { - throw new SlackApiError( - `Slack API error: ${response.data.error || 'Unknown error'}`, + if (!json.ok) { + throw new SlackError( + `Slack API error: ${json.error || 'Unknown error'}`, endpoint, - new Error(response.data.error_description || response.data.error || 'Unknown error'), - response.status, - response.data + new Error(json.error_description || json.error || 'Unknown error'), + res.status, + json as SlackApiResponse ); } // Cache successful responses if (!options.skipCache) { const cacheKey = this.getCacheKey(endpoint, cleanParams); - this.cacheResponse(cacheKey, response, options.cacheExpiryMs); - } - - return response; - } catch (error) { - // Handle axios errors - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - throw new SlackApiError( - `Slack API request failed: ${axiosError.message}`, - endpoint, - axiosError, - axiosError.response?.status, - axiosError.response?.data - ); + this.cacheResponse(cacheKey, json, options.cacheExpiryMs); } - // Handle other errors - throw new SlackApiError( + return json; + } catch (error: any) { + throw new SlackError( `Error in Slack API GET to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, endpoint, error instanceof Error ? error : new Error(String(error)) From a8350ecfaa0d9caff07db2ed062ff50e0de97707 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 10 May 2025 17:19:32 -0600 Subject: [PATCH 19/29] refactor: Use `unknown` instead of `any` --- src/plugins/slack-now-playing/main.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index cc25f4e907..ba709eb35b 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -354,15 +354,11 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon return false; } - // Make the API request - the client now handles API errors internally try { // The post method now returns a properly typed response await client.post<{ ok: boolean }>('emoji.add', formData, true); - - // If we got here, the request was successful - // Emoji uploaded successfully return true; - } catch (apiError: any) { + } catch (apiError: unknown) { // Handle specific API error types if (apiError instanceof SlackError && apiError.responseData) { const errorCode = apiError.responseData.error; From 7e89a0d87021d9393eba6ca03601fad8108b416c Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 10 May 2025 17:19:43 -0600 Subject: [PATCH 20/29] chore: remove unused @types/form-data dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 0d78fa0795..bc3ae1a7d2 100644 --- a/package.json +++ b/package.json @@ -315,7 +315,6 @@ "@stylistic/eslint-plugin-js": "4.2.0", "@total-typescript/ts-reset": "0.6.1", "@types/electron-localshortcut": "3.1.3", - "@types/form-data": "^2.5.2", "@types/howler": "2.2.12", "@types/html-to-text": "9.0.4", "@types/semver": "7.7.0", From 94910d6d81272c829d5babe969bac53102c7d81d Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 17 May 2025 13:58:11 -0600 Subject: [PATCH 21/29] refactor: Use native JS APIs to send emoji to Slack --- package.json | 1 - pnpm-lock.yaml | 20 -------------------- src/plugins/slack-now-playing/main.ts | 12 ++++++------ 3 files changed, 6 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index bc3ae1a7d2..6c9969c78b 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,6 @@ "fetch-blob": "^4.0.0", "file-type": "^20.5.0", "filenamify": "6.0.0", - "formdata-node": "^6.0.3", "hanja": "1.1.4", "happy-dom": "17.4.4", "hono": "4.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc5bcc7bc1..833d523235 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,6 @@ importers: filenamify: specifier: 6.0.0 version: 6.0.0 - formdata-node: - specifier: ^6.0.3 - version: 6.0.3 hanja: specifier: 1.1.4 version: 1.1.4 @@ -256,9 +253,6 @@ importers: '@types/electron-localshortcut': specifier: 3.1.3 version: 3.1.3 - '@types/form-data': - specifier: ^2.5.2 - version: 2.5.2 '@types/howler': specifier: 2.2.12 version: 2.2.12 @@ -1252,10 +1246,6 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/form-data@2.5.2': - resolution: {integrity: sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==} - deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed. - '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -2548,10 +2538,6 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} - formdata-node@6.0.3: - resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} - engines: {node: '>= 18'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -5696,10 +5682,6 @@ snapshots: '@types/estree@1.0.7': {} - '@types/form-data@2.5.2': - dependencies: - form-data: 4.0.2 - '@types/fs-extra@9.0.13': dependencies: '@types/node': 22.13.5 @@ -7311,8 +7293,6 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - formdata-node@6.0.3: {} - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index ba709eb35b..85ce5265d0 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -3,8 +3,6 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { SlackApiClient, SlackError } from './slack-api-client'; -import { FormData } from 'formdata-node'; -import { fileFromPath } from 'formdata-node/file-from-path'; import { createBackend } from '@/utils'; import registerCallback, { SongInfoEvent } from '@/providers/song-info'; import { t } from '@/i18n'; @@ -340,14 +338,16 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon return false; } - // Prepare the form data for the API request using formdata-node + // Prepare the form data for the API request using native Node.js APIs const formData = new FormData(); formData.append('mode', 'data'); formData.append('name', config.emojiName); try { - // Use fileFromPath to get a proper File object for formdata-node - const imageFile = await fileFromPath(filePath); - formData.append('image', imageFile); + // Read the file as a Buffer and append as a Blob + const fileBuffer = await fs.promises.readFile(filePath); + // Use path.basename to preserve the original filename if desired + const filename = path.basename(filePath) || 'emoji.png'; + formData.append('image', new Blob([fileBuffer]), filename); } catch (fileError: any) { console.error(`Error preparing album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); From 8a680348e2ea8fd0657c23c0b64eae02b665a4b7 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 17 May 2025 14:48:04 -0600 Subject: [PATCH 22/29] fix: handle FormData file upload in environments without File/Blob support --- package.json | 1 - src/plugins/slack-now-playing/main.ts | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6c9969c78b..e37f9ff8ae 100644 --- a/package.json +++ b/package.json @@ -273,7 +273,6 @@ "es-hangul": "2.3.3", "fast-average-color": "9.5.0", "fast-equals": "5.2.2", - "fetch-blob": "^4.0.0", "file-type": "^20.5.0", "filenamify": "6.0.0", "hanja": "1.1.4", diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index 85ce5265d0..4927d9dc5b 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -343,11 +343,18 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon formData.append('mode', 'data'); formData.append('name', config.emojiName); try { - // Read the file as a Buffer and append as a Blob + // Read the file as a Buffer and append directly to FormData const fileBuffer = await fs.promises.readFile(filePath); - // Use path.basename to preserve the original filename if desired const filename = path.basename(filePath) || 'emoji.png'; - formData.append('image', new Blob([fileBuffer]), filename); + let imageFile; + if (typeof File !== 'undefined') { + imageFile = new File([fileBuffer], filename); + } else if (typeof Blob !== 'undefined') { + imageFile = new Blob([fileBuffer]); + } else { + throw new Error('Neither File nor Blob is available in this environment'); + } + formData.append('image', imageFile, filename); } catch (fileError: any) { console.error(`Error preparing album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); From 2e287c756e12eebedac7625a8918aedf08e27540 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 17 May 2025 14:56:45 -0600 Subject: [PATCH 23/29] chore: remove file-type dependency from package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index e37f9ff8ae..0feccdbf89 100644 --- a/package.json +++ b/package.json @@ -273,7 +273,6 @@ "es-hangul": "2.3.3", "fast-average-color": "9.5.0", "fast-equals": "5.2.2", - "file-type": "^20.5.0", "filenamify": "6.0.0", "hanja": "1.1.4", "happy-dom": "17.4.4", From 1fff6702ce84066620a83d390d4d2c26b8e9efdf Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 17 May 2025 14:58:25 -0600 Subject: [PATCH 24/29] Update pnpm-lock.yaml --- pnpm-lock.yaml | 1275 +++++++++++++++++++++++------------------------- 1 file changed, 608 insertions(+), 667 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 833d523235..105acf9ddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: dependencies: '@electron-toolkit/tsconfig': specifier: 1.0.1 - version: 1.0.1(@types/node@22.13.5) + version: 1.0.1(@types/node@22.15.18) '@electron/remote': specifier: 2.1.2 version: 2.1.2(electron@34.5.3) @@ -135,12 +135,6 @@ importers: fast-equals: specifier: 5.2.2 version: 5.2.2 - fetch-blob: - specifier: ^4.0.0 - version: 4.0.0 - file-type: - specifier: ^20.5.0 - version: 20.5.0 filenamify: specifier: 6.0.0 version: 6.0.0 @@ -294,7 +288,7 @@ importers: version: 4.0.0 electron-vite: specifier: 3.1.0 - version: 3.1.0(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + version: 3.1.0(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) esbuild: specifier: 0.25.3 version: 0.25.3 @@ -315,7 +309,7 @@ importers: version: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.25.1) eslint-plugin-prettier: specifier: 5.2.6 - version: 5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.2) + version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3) glob: specifier: 11.0.2 version: 11.0.2 @@ -339,16 +333,16 @@ importers: version: 6.0.5 vite: specifier: 6.3.3 - version: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) + version: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) vite-plugin-inspect: specifier: 11.0.1 - version: 11.0.1(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + version: 11.0.1(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) vite-plugin-resolve: specifier: 2.5.2 version: 2.5.2 vite-plugin-solid: specifier: 2.11.6 - version: 2.11.6(solid-js@1.9.5)(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + version: 2.11.6(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) ws: specifier: 8.18.1 version: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -370,73 +364,73 @@ packages: peerDependencies: zod: ^3.20.2 - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.8': - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + '@babel/compat-data@7.27.2': + resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.10': - resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + '@babel/core@7.27.1': + resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.0': - resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.18.6': resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + '@babel/helper-module-transforms@7.27.1': + resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.25.9': - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-arrow-functions@7.25.9': - resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -445,20 +439,20 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.0': - resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} - '@bufbuild/protobuf@2.2.3': - resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==} + '@bufbuild/protobuf@2.4.0': + resolution: {integrity: sha512-RN9M76x7N11QRihKovEglEjjVCQEA9PRBVnDgk9xw8JHLrcUrp4FpAVSPSH91cNbcTft3u2vpLN4GMbiKY9PJw==} '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} @@ -468,8 +462,8 @@ packages: resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} engines: {node: '>=18'} - '@discordjs/rest@2.4.3': - resolution: {integrity: sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==} + '@discordjs/rest@2.5.0': + resolution: {integrity: sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==} engines: {node: '>=18'} '@discordjs/util@1.1.1': @@ -486,8 +480,8 @@ packages: engines: {node: '>=10.12.0'} hasBin: true - '@electron/asar@3.3.1': - resolution: {integrity: sha512-WtpC/+34p0skWZiarRjLAyqaAX78DofhDxnREy/V5XHfu1XEXbFCSSMcDQ6hNCPJFaPy8/NnUgYuf9uiCkvKPg==} + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} engines: {node: '>=10.12.0'} hasBin: true @@ -528,19 +522,19 @@ packages: resolution: {integrity: sha512-mqY1szx5/d5YLvfCDWWoJdkSIjIz+NdWN4pN0r78lYiE7De+slLpuF3lVxIT+hlJnwk5sH2wFRMl6/oUgUVO3A==} engines: {node: '>=16.4'} - '@electron/windows-sign@1.2.1': - resolution: {integrity: sha512-YfASnrhJ+ve6Q43ZiDwmpBgYgi2u0bYjeAVi2tDfN7YWAKO8X9EEOuPGtqbJpPLM6TfAHimghICjWe2eaJ8BAg==} + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} engines: {node: '>=14.14'} hasBin: true - '@emnapi/core@1.4.0': - resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==} + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} - '@emnapi/runtime@1.4.0': - resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - '@emnapi/wasi-threads@1.0.1': - resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} '@esbuild/aix-ppc64@0.25.3': resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} @@ -692,8 +686,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -706,8 +700,8 @@ packages: resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + '@eslint/config-helpers@0.2.2': + resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.13.0': @@ -741,8 +735,8 @@ packages: resolution: {integrity: sha512-LILAKTrU3Rga2iXLsF9jeFxe2hNQFjWlrKuXPWSdCFeQ7Kg69fO4WwjNJ0CzjOyO6qtndRQMNKqf//N4fLYUBA==} engines: {node: '>=12.16.1'} - '@floating-ui/core@1.6.9': - resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + '@floating-ui/core@1.7.0': + resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} '@floating-ui/dom@1.6.13': resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} @@ -756,8 +750,8 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@ghostery/adblocker-content@2.5.1': - resolution: {integrity: sha512-Im0GiRxKdyWWWiIQBBOzA4nHecvPjOvMvT+GqsTHjcVqz+xFC22Jw1BPoniOjVYnOhS/XxtQZOdjm6A2Cbffew==} + '@ghostery/adblocker-content@2.5.2': + resolution: {integrity: sha512-H3e4QZsom7HqVgIBLaoHriqRh27MyXgwC43ClidOXXbCtKn6h7c3wc9TnQssQpXpcyV7HRPmWjMtADzUc+yYKg==} '@ghostery/adblocker-electron-preload@2.5.1': resolution: {integrity: sha512-QtTU/H8XmjK1VsK2Ua2Tk79RFxL5B1KltclWolJmTgVZeXUk9eJj0Wpo/hwFcFigVfXJ8Pbjbym9iftvPks6fQ==} @@ -769,11 +763,11 @@ packages: peerDependencies: electron: '>11' - '@ghostery/adblocker-extended-selectors@2.5.1': - resolution: {integrity: sha512-UR9p/uEUmTXyQKkfCKwYesLkqJAGIyeFS2jDzIGgYDRyHGVlxu7Gcl/cdZehikTzBtrGtzc3X69amaUjvUyUJw==} + '@ghostery/adblocker-extended-selectors@2.5.2': + resolution: {integrity: sha512-Z2MQ4BiPTPG3cI1CFF1cE0IywL1EM2KGnOVkKEx62fnkO0aRyvAeja0jhQvjENtY/hixWLrsSg3MU95Clvq23g==} - '@ghostery/adblocker@2.5.1': - resolution: {integrity: sha512-/sSSVLwb3ojIFw0owwffsGdCDOeMI02L/nXFIJD2exRAwG8hOETYwCGO7KwSyH2bnVTHtnIQYD1+hfeqQN+I5A==} + '@ghostery/adblocker@2.5.2': + resolution: {integrity: sha512-/SLxUGPd1JISNGOPsxKfbso+uylDEvEp3umF5gQ3x8YgsEZzD6zYx7H4ZQxAvG1pZIr4p6N9PiAf2N88T1Wo1Q==} '@hono/node-server@1.14.1': resolution: {integrity: sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==} @@ -815,8 +809,8 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} '@isaacs/cliui@8.0.2': @@ -978,8 +972,8 @@ packages: resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@0.2.9': - resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} + '@napi-rs/wasm-runtime@0.2.10': + resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1018,8 +1012,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.2.0': - resolution: {integrity: sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==} + '@pkgr/core@0.2.4': + resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@playwright/test@1.52.0': @@ -1027,26 +1021,26 @@ packages: engines: {node: '>=18'} hasBin: true - '@polka/url@1.0.0-next.28': - resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@remusao/guess-url-type@2.0.0': - resolution: {integrity: sha512-L98gV/X/GESt5Tgqq/PxpZYClVqeq6/5InrRKl4elq4qXbdZjHlNTgRhXb1xIaUBkikzv410sXw3QaBUYyXt8g==} + '@remusao/guess-url-type@2.1.0': + resolution: {integrity: sha512-zI3dlTUxpjvx2GCxp9nLOSK5yEIqDCpxlAVGwb2Y49RKkS72oeNaxxo+VWS5+XQ5+Mf8Zfp9ZXIlk+G5eoEN8A==} - '@remusao/small@2.0.0': - resolution: {integrity: sha512-1ksGCbl1hSeO4CV/uk0qv2vUdZ1ZRdIMzSn0HFsHTJnmWSDk2li+T5eCI9BtweYe0uVQhCvbMjk/3qwsN6fuYg==} + '@remusao/small@2.1.0': + resolution: {integrity: sha512-Y1kyjZp7JU7dXdyOdxHVNfoTr1XLZJTyQP36/esZUU/WRWq9XY0PV2HsE3CsIHuaTf4pvgWv2pvzvnZ//UHIJQ==} - '@remusao/smaz-compress@2.1.0': - resolution: {integrity: sha512-IyuzXxd5F1p5WAvA6Td9ny+wh3sj9szMmdZtQ8Fb5/yE4lIwujXCz0e702u62r3XU47qsQcR/CjSRaw65u7iGA==} + '@remusao/smaz-compress@2.2.0': + resolution: {integrity: sha512-TXpTPgILRUYOt2rEe0+9PC12xULPvBqeMpmipzB9A7oM4fa9Ztvy9lLYzPTd7tiQEeoNa1pmxihpKfJtsxnM/w==} - '@remusao/smaz-decompress@2.1.0': - resolution: {integrity: sha512-TlM/ibBOMiCRniuBjv4x4nIcVbOb1o6DdK+p5OM+zHNndEu9za2C1Bd+PSqnsVCn15Fv2HcLVWGpqh2hKs9uuw==} + '@remusao/smaz-decompress@2.2.0': + resolution: {integrity: sha512-ERAPwxPaA0/yg4hkNU7T2S+lnp9jj1sApcQMtOyROvOQyo+Zuh6Hn/oRcXr8mmjlYzyRaC7E6r3mT1nrdHR6pg==} - '@remusao/smaz@2.1.0': - resolution: {integrity: sha512-hTn/ZuBY4LYaqvprTdW3U+uU4xrw5JusxqHIhUzroQlrT6l4LFQRi+aJE/SOv09iS7eO/pqg6Ec3Go+gKiWH8A==} + '@remusao/smaz@2.2.0': + resolution: {integrity: sha512-eSd3Qs0ELP/e7tU1SI5RWXcCn9KjDgvBY+KtWbL4i2QvvHhJOfdIt4v0AA3S5BbLWAr5dCEC7C4LUfogDm6q/Q==} - '@remusao/trie@2.0.0': - resolution: {integrity: sha512-YmVfZrd+igKXJMuAvsNdDnUAyoNw7M+9e2ij6UsaZFZGQzLd75pbxhiuFmdphEXi/s3uXe25+wtFRWjLA2Ho0Q==} + '@remusao/trie@2.1.0': + resolution: {integrity: sha512-Er3Q8q0/2OcCJPQYJOPLmCuqO0wu7cav3SPtpjlxSbjFi1x+A1pZkkLD6c9q2rGEkGW/tkrRzfrhNMt8VQjzXg==} '@rollup/rollup-android-arm-eabi@4.40.0': resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} @@ -1174,18 +1168,18 @@ packages: resolution: {integrity: sha512-jT2OWwpajtXTb6opnaIwmBTMpQtKUwl2Ro1zApxIIrpZJon71kZIv6GZSc08LzKO2lpTqUjvD+i7Z2hGuG42KQ==} engines: {node: '>=v18'} - '@solid-primitives/refs@1.1.0': - resolution: {integrity: sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA==} + '@solid-primitives/refs@1.1.1': + resolution: {integrity: sha512-MIQ7Bh59IiT9NDQPf6iWRnPe0RgKggEjF0H+iMoIi1KBCcp4Mfss2IkUWYPr9wqQg963ZQFbcg5D6oN9Up6Mww==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/transition-group@1.1.0': - resolution: {integrity: sha512-pL1sEPCHuC4V+Yh+SQsKSPuGDYrZbLJYSkk3AB4TZrWhptEJUS0IHoi7BAynYcMiULbvMMVKFbeFHqINZq0+ig==} + '@solid-primitives/transition-group@1.1.1': + resolution: {integrity: sha512-yf8mheMunnAkPSH2WNlemdSR2mrBar0Hw2FenZCqr10iKrI4sUiERIOR4nnFNnUK73BVwAA/xeYbiOk6s36fvw==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/utils@6.3.0': - resolution: {integrity: sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A==} + '@solid-primitives/utils@6.3.1': + resolution: {integrity: sha512-4/Z59nnwu4MPR//zWZmZm2yftx24jMqQ8CSd/JobL26TPfbn4Ph8GKNVJfGJWShg1QB98qObJSskqizbTvcLLA==} peerDependencies: solid-js: ^1.6.12 @@ -1199,10 +1193,6 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} - engines: {node: '>=18'} - '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1222,14 +1212,14 @@ packages: '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} @@ -1240,9 +1230,6 @@ packages: '@types/electron-localshortcut@3.1.3': resolution: {integrity: sha512-D+CRdDTRZ4/9UmcSaZ5qvW4uq2VyyVmqsH9cdNReB4CL6MSIgyhr9w2PKeNEb0J/ZS7db7irJM/+ZiA5uSQsLw==} - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -1273,11 +1260,11 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@20.17.31': - resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==} + '@types/node@20.17.47': + resolution: {integrity: sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==} - '@types/node@22.13.5': - resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} + '@types/node@22.15.18': + resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1347,83 +1334,88 @@ packages: resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-darwin-arm64@1.6.3': - resolution: {integrity: sha512-+BbDAtwT4AVUyGIfC6SimaA6Mi/tEJCf5OYV5XQg7WIOW0vyD15aVgDLvsQscIZxgz42xB6DDqR7Kv6NBQJrEg==} + '@unrs/resolver-binding-darwin-arm64@1.7.2': + resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.6.3': - resolution: {integrity: sha512-q6qMXI8wT0u0GUns/L26kYHdX2du4yEhwxrXjPj/egvysI8XqcTyjnbWQm3NSJPw0Un2wvKPh0WuoTSJEZgbqw==} + '@unrs/resolver-binding-darwin-x64@1.7.2': + resolution: {integrity: sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.6.3': - resolution: {integrity: sha512-/7xs7QNNW17VZrFBf+2C95G72rA5c0YGtR18pvWrzM2tVPLrTsKnLl32hi3CG7F6cwwYRy7h61BIkMHh7qaZkw==} + '@unrs/resolver-binding-freebsd-x64@1.7.2': + resolution: {integrity: sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.6.3': - resolution: {integrity: sha512-2xv5cUQCt+eYuq5tPF4AHStpzE8i8qdYnhitpvDv9vxzOZ5a0sdzgA8WHYgFe15dP469YOSivenMMdpuRcgE9Q==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + resolution: {integrity: sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.6.3': - resolution: {integrity: sha512-4KaZxKIeFt/jAOD/zuBOLb5yyZk/XG9FKf5IXpDP21NcYxeus/os6w+NCK7wjSJKbOpHZhwfkAYLkfujkAOFkw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + resolution: {integrity: sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.6.3': - resolution: {integrity: sha512-dJoZsZoWwvfS+khk0jkX6KnLL1T2vbRfsxinOR3PghpRKmMTnasEVAxmrXLQFNKqVKZV/mU7gHzWhiBMhbq3bw==} + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-arm64-musl@1.6.3': - resolution: {integrity: sha512-2Y6JcAY9e557rD6O53Zmeblrfu48vQfl5CrrKjt0/2J1Op/pKX3WI8TOh0gs5T4qX9uJDqdte11SNUssckdfUA==} + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-ppc64-gnu@1.6.3': - resolution: {integrity: sha512-kvcEe+j0De/DEfTNkte2xtmwSL4/GMesArcqmSgRqoOaGknUYY3whJ/3GygYKNMe82vvao4PaQkBlCrxhi88wQ==} + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==} cpu: [ppc64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-gnu@1.6.3': - resolution: {integrity: sha512-fruY8swKre2H0J96h8HE+kN3iUnDR3VDd2wxBn4BxDw+5g7GOHBz5x1533l9mqAqHI4b2dMBECI4RtQdMOiBeQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-s390x-gnu@1.6.3': - resolution: {integrity: sha512-1w0eaSxm9e69TEj9eArZDPQ7mL2VL6Bb4AXeLOdQoe5SNQpZaL6RlwGm7ss9xErwC7c9Hvob/ZZF7i8xYT55zg==} + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==} cpu: [s390x] os: [linux] - '@unrs/resolver-binding-linux-x64-gnu@1.6.3': - resolution: {integrity: sha512-ymUqs8AQyHTQQ50aN7EcMV47gKh5yKg8a0+SWSuDZEl6eGEOKn590D/iMDydS5KoWbMTy6/pBipS4vsPUEjYVw==} + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-linux-x64-musl@1.6.3': - resolution: {integrity: sha512-LSfz1cguLZD+c00aTVbtrqX1x1sIR38M2lLYW3CZTGfippkg56Hf8kejHPA8H26OwB71c9/W78BCbgcdnEW+jQ==} + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-wasm32-wasi@1.6.3': - resolution: {integrity: sha512-gehKZDmNDS2QTxefwPBLi0RJgOQ0dIoD/osCcNboDb3+ZKcbSMBaF3+4R5vj+XdV0QBdZg3vXwdwZswfEkQOcA==} + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.6.3': - resolution: {integrity: sha512-CzTmpDxwkoYl69stmlJzcVWITQEC6Vs8ASMZMEMbFO+q1Dw0GtpRjAA6X76zGcLOADDwzugx1vpT6YXarrhpTA==} + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + resolution: {integrity: sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.6.3': - resolution: {integrity: sha512-j+n1gWkfu4Q/octUHXU1p1IOrh+B27vpA7ec81RB6nXCml5u7F0B7SrCZU+HqajxjVqgEQEYOcRCb1yzfwfsWw==} + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + resolution: {integrity: sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.6.3': - resolution: {integrity: sha512-n33drkd84G5Mu2BkUGawZXmm+IFPuRv7GpODfwEBs/CzZq2+BIZyAZmb03H9IgNbd7xaohZbtZ4/9Gb0xo5ssw==} + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + resolution: {integrity: sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==} cpu: [x64] os: [win32] @@ -1445,8 +1437,8 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - abbrev@3.0.0: - resolution: {integrity: sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} abort-controller@3.0.0: @@ -1462,8 +1454,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1558,8 +1550,8 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -1617,8 +1609,8 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} - babel-plugin-jsx-dom-expressions@0.39.7: - resolution: {integrity: sha512-8GzVmFla7jaTNWW8W+lTMl9YGva4/06CtwJjySnkYtt8G1v9weCzc2SuF1DfrudcCNb2Doetc1FRg33swBYZCA==} + babel-plugin-jsx-dom-expressions@0.39.8: + resolution: {integrity: sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ==} peerDependencies: '@babel/core': ^7.20.12 @@ -1629,8 +1621,8 @@ packages: resolution: {integrity: sha512-fZI/4cYneinlj2k/FsXw0/lTWSC5KKoepUueS1g25Gb5vx3GrRyaVwxWCshYqx11GEU4mZnbbFhee8vpquFS2w==} engines: {node: '>=8', npm: '>=6'} - babel-preset-solid@1.9.5: - resolution: {integrity: sha512-85I3osODJ1LvZbv8wFozROV1vXq32BubqHXAGu73A//TRs3NLI1OFP83AQBUTSQHwgZQmARjHlJciym3we+V+w==} + babel-preset-solid@1.9.6: + resolution: {integrity: sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg==} peerDependencies: '@babel/core': ^7.0.0 @@ -1643,8 +1635,8 @@ packages: bgutils-js@3.2.0: resolution: {integrity: sha512-CacO15JvxbclbLeCAAm9DETGlLuisRGWpPigoRvNsccSCPEC4pwYwA2g2x/pv7Om/sk79d4ib35V5HHmxPBpDg==} - birpc@2.2.0: - resolution: {integrity: sha512-1/22obknhoj56PcE+pZPp6AbWDdY55M81/ofpPW3Ltlp9Eh4zoFFLswvZmNpRTb790CY5tsNfgbYeNOqIARJfQ==} + birpc@2.3.0: + resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1676,8 +1668,8 @@ packages: browser-extension-url-match@1.2.0: resolution: {integrity: sha512-+O/t71m1opNU5KG/bJkeNLvXLp0OxlFekjdR8w6waUOyWhkL6+bnQ6dCDoJxc6YF6ZQM0r64ag/d9K4m05ULsg==} - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + browserslist@4.24.5: + resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1750,8 +1742,8 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} callsites@3.1.0: @@ -1762,8 +1754,8 @@ packages: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} - caniuse-lite@1.0.30001701: - resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} + caniuse-lite@1.0.30001718: + resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} chalk-template@0.4.0: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} @@ -1834,8 +1826,8 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-convert@3.0.1: - resolution: {integrity: sha512-5kQah2eolfQV7HCrxtsBBArPfT5dwaKYMCXeMQsdRO7ihTO/cuNLGjd50ITCDn+ZU/YbS0Go64SjP9154eopxg==} + color-convert@3.1.0: + resolution: {integrity: sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA==} engines: {node: '>=14.6'} color-name@1.1.4: @@ -1974,8 +1966,8 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2050,8 +2042,8 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} detect-node@2.1.0: @@ -2100,8 +2092,8 @@ packages: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} - dotenv@16.4.7: - resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} doublearray@0.0.2: @@ -2169,8 +2161,8 @@ packages: resolution: {integrity: sha512-Ok0bF13WWdTzZi9rCtPN8wUfwx+yDMmV6PAnCMqjNRKEXHmklW/rV+6DofV/Vf5qoAh+Bl9Bj7dQ+0W+IL2psg==} engines: {node: '>=20'} - electron-to-chromium@1.5.109: - resolution: {integrity: sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==} + electron-to-chromium@1.5.155: + resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==} electron-unhandled@4.0.1: resolution: {integrity: sha512-6BsLnBg+i96eUnbaIFZyYdyfNX3f80/Nlfqy34YEMxXT9JP3ddNsNnUeiOF8ezN4+et4t4D37gjghKTP0V3jyw==} @@ -2218,6 +2210,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2481,13 +2477,6 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - fetch-blob@4.0.0: - resolution: {integrity: sha512-nPmnhRmpNMjYWnp9EBMGs6z5lq9RXed5W1vuZcECrsDVQInM8AMQSooVb3X183Aole60adzjWbH9qlRFWzDDTA==} - engines: {node: '>=16.7'} - - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2496,10 +2485,6 @@ packages: resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} engines: {node: '>=10'} - file-type@20.5.0: - resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==} - engines: {node: '>=18'} - filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -2754,8 +2739,8 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} @@ -2812,8 +2797,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.3: - resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==} + ignore@7.0.4: + resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} engines: {node: '>= 4'} image-q@4.0.0: @@ -3076,8 +3061,8 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsbi@4.3.0: - resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==} + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -3204,8 +3189,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3219,8 +3204,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - magic-bytes.js@1.10.0: - resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} + magic-bytes.js@1.12.1: + resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3271,8 +3256,8 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} mime-types@2.1.18: @@ -3371,8 +3356,8 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} - minizlib@3.0.1: - resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} engines: {node: '>= 18'} mkdirp@0.5.6: @@ -3399,13 +3384,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.1.5: - resolution: {integrity: sha512-HI5bHONOUYqV+FJvueOSgjRxHTLB25a3xIv59ugAxFe7xRNbW96hyYbMbsKzl+QvFV9mN/SrtHwiU+vYhMwA7Q==} + napi-postinstall@0.2.4: + resolution: {integrity: sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -3424,15 +3409,15 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-abi@3.74.0: - resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} engines: {node: '>=10'} node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - node-api-version@0.2.0: - resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} @@ -3527,8 +3512,8 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - open@10.1.0: - resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + open@10.1.2: + resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} engines: {node: '>=18'} openapi3-ts@4.4.0: @@ -3593,8 +3578,8 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -3656,10 +3641,6 @@ packages: resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} engines: {node: '>=8'} - peek-readable@7.0.0: - resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} - engines: {node: '>=18'} - peerjs-js-binarypack@2.1.0: resolution: {integrity: sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==} engines: {node: '>= 14.0.0'} @@ -3748,8 +3729,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.5.2: - resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true @@ -3905,10 +3886,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -3985,14 +3962,14 @@ packages: resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} engines: {node: '>=10'} - seroval-plugins@1.2.1: - resolution: {integrity: sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==} + seroval-plugins@1.3.1: + resolution: {integrity: sha512-dOlUoiI3fgZbQIcj6By+l865pzeWdP3XCSLdI3xlKnjCk5983yLWPsXytFOUI0BUZKG9qwqbj78n9yVcVwUqaQ==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.2.1: - resolution: {integrity: sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==} + seroval@1.3.1: + resolution: {integrity: sha512-F+T9EQPdLzgdewgxnBh4mSc+vde+EOkU6dC9BDuu/bfGb+UyUlqM6t8znFCTPQSuai/ZcfFg0gu79h+bVW2O0w==} engines: {node: '>=10'} serve-handler@6.1.6: @@ -4205,10 +4182,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strtok3@10.2.2: - resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} - engines: {node: '>=18'} - strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} @@ -4228,8 +4201,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.11.1: - resolution: {integrity: sha512-fWZqNBZNNFp/7mTUy1fSsydhKsAKJ+u90Nk7kOK5Gcq9vObaqLBLjWFDBkyVU9Vvc6Y71VbOevMuGhqv02bT+Q==} + synckit@0.11.6: + resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==} engines: {node: ^14.18.0 || >=16.0.0} tar@6.2.1: @@ -4263,11 +4236,17 @@ packages: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} - tldts-core@6.1.79: - resolution: {integrity: sha512-HM+Ud/2oQuHt4I43Nvjc213Zji/z25NSH5OkJskJwHXNtYh9DTRlHMDFhms9dFMP7qyve/yVaXFIxmcJ7TdOjw==} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.0.7: + resolution: {integrity: sha512-ECqb8imSroX1UmUuhRBNPkkmtZ8mHEenieim80UVxG0M5wXVjY2Fp2tYXCPvk+nLy1geOhFpeD5YQhM/gF63Jg==} - tldts-experimental@6.1.79: - resolution: {integrity: sha512-qAh6vDChn8fkkAaro/H632Af0yDQdcKGDQLYdwsv/c37VqKLiEO8loATjAgZPO2mwEgEYgzmxqtSLu293GjCTA==} + tldts-experimental@6.1.86: + resolution: {integrity: sha512-X3N3+SrwSajvANDyIBFa6tf/nO0VoqaXvvINSnQkZMGbzNlD+9G7Xb24Mtk3ZBVZJRGY7UynAJJL8kRVt6Z46Q==} + + tldts-experimental@7.0.7: + resolution: {integrity: sha512-V055ViO8G6PbTBfaiL1Utq/MLUqFZKhJ1c9+T8t+c1uPmVSAl7rR6ib8y0lleN18niKV6f1/zmMlAhpFEaPY9w==} tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} @@ -4284,10 +4263,6 @@ packages: resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} engines: {node: '>=10'} - token-types@6.0.0: - resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} - engines: {node: '>=14.16'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4295,8 +4270,8 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - ts-api-utils@2.0.1: - resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4326,8 +4301,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-fest@4.35.0: - resolution: {integrity: sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} typed-array-buffer@1.0.3: @@ -4369,11 +4344,11 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@5.28.5: - resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} undici@6.21.1: @@ -4412,8 +4387,8 @@ packages: resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} engines: {node: '>=18.12.0'} - unrs-resolver@1.6.3: - resolution: {integrity: sha512-mYNIMmxlDcaepmUTNrBu2tEB/bRkLBUeAhke8XOnXYqSu/9dUk4cdFiJG1N4d5Q7Fii+9MpgavkxJpnXPqNhHw==} + unrs-resolver@1.7.2: + resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==} unzip-crx-3@0.2.0: resolution: {integrity: sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==} @@ -4553,8 +4528,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webrtc-adapter@9.0.1: - resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==} + webrtc-adapter@9.0.3: + resolution: {integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} whatwg-mimetype@3.0.0: @@ -4576,8 +4551,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -4660,9 +4635,9 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} - engines: {node: '>= 14'} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@21.1.1: @@ -4705,125 +4680,125 @@ snapshots: openapi3-ts: 4.4.0 zod: 3.24.3 - '@babel/code-frame@7.26.2': + '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.26.8': {} + '@babel/compat-data@7.27.2': {} - '@babel/core@7.26.10': + '@babel/core@7.27.1': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.27.0': + '@babel/generator@7.27.1': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.26.5': + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 + '@babel/compat-data': 7.27.2 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.5 lru-cache: 5.1.1 semver: 6.3.1 '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.27.1 - '@babel/helper-module-imports@7.25.9': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.26.5': {} + '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.0': + '@babel/helpers@7.27.1': dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 - '@babel/parser@7.27.0': + '@babel/parser@7.27.2': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.27.1 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.10)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.1)': dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.27.0': + '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 - '@babel/traverse@7.27.0': + '@babel/traverse@7.27.1': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - debug: 4.4.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.27.0': + '@babel/types@7.27.1': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 - '@bufbuild/protobuf@2.2.3': {} + '@bufbuild/protobuf@2.4.0': {} '@develar/schema-utils@2.6.5': dependencies: @@ -4832,23 +4807,23 @@ snapshots: '@discordjs/collection@2.1.1': {} - '@discordjs/rest@2.4.3': + '@discordjs/rest@2.5.0': dependencies: '@discordjs/collection': 2.1.1 '@discordjs/util': 1.1.1 '@sapphire/async-queue': 1.5.5 '@sapphire/snowflake': 3.5.5 '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.120 - magic-bytes.js: 1.10.0 + discord-api-types: 0.38.1 + magic-bytes.js: 1.12.1 tslib: 2.8.1 undici: 6.21.1 '@discordjs/util@1.1.1': {} - '@electron-toolkit/tsconfig@1.0.1(@types/node@22.13.5)': + '@electron-toolkit/tsconfig@1.0.1(@types/node@22.15.18)': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.15.18 '@electron/asar@3.2.18': dependencies: @@ -4856,7 +4831,7 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - '@electron/asar@3.3.1': + '@electron/asar@3.4.1': dependencies: commander: 5.1.0 glob: 7.2.3 @@ -4870,7 +4845,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.0 + debug: 4.4.1 env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -4900,7 +4875,7 @@ snapshots: '@electron/notarize@2.5.0': dependencies: - debug: 4.4.0 + debug: 4.4.1 fs-extra: 9.1.0 promise-retry: 2.0.1 transitivePeerDependencies: @@ -4909,7 +4884,7 @@ snapshots: '@electron/osx-sign@1.3.1': dependencies: compare-version: 0.1.2 - debug: 4.4.0 + debug: 4.4.1 fs-extra: 10.1.0 isbinaryfile: 4.0.10 minimist: 1.2.8 @@ -4922,12 +4897,12 @@ snapshots: '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.0 - detect-libc: 2.0.3 + debug: 4.4.1 + detect-libc: 2.0.4 fs-extra: 10.1.0 got: 11.8.6 - node-abi: 3.74.0 - node-api-version: 0.2.0 + node-abi: 3.75.0 + node-api-version: 0.2.1 ora: 5.4.1 read-binary-file-arch: 1.0.6 semver: 7.7.1 @@ -4943,9 +4918,9 @@ snapshots: '@electron/universal@2.0.2': dependencies: - '@electron/asar': 3.3.1 + '@electron/asar': 3.4.1 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.0 + debug: 4.4.1 dir-compare: 4.2.0 fs-extra: 11.3.0 minimatch: 9.0.5 @@ -4953,10 +4928,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@electron/windows-sign@1.2.1': + '@electron/windows-sign@1.2.2': dependencies: cross-dirname: 0.1.0 - debug: 4.4.0 + debug: 4.4.1 fs-extra: 11.3.0 minimist: 1.2.8 postject: 1.0.0-alpha.6 @@ -4964,18 +4939,18 @@ snapshots: - supports-color optional: true - '@emnapi/core@1.4.0': + '@emnapi/core@1.4.3': dependencies: - '@emnapi/wasi-threads': 1.0.1 + '@emnapi/wasi-threads': 1.0.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.0': + '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.1': + '@emnapi/wasi-threads@1.0.2': dependencies: tslib: 2.8.1 optional: true @@ -5055,7 +5030,7 @@ snapshots: '@esbuild/win32-x64@0.25.3': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.25.1)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.25.1)': dependencies: eslint: 9.25.1 eslint-visitor-keys: 3.4.3 @@ -5065,12 +5040,12 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.1': {} + '@eslint/config-helpers@0.2.2': {} '@eslint/core@0.13.0': dependencies: @@ -5079,7 +5054,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.1 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -5109,13 +5084,13 @@ snapshots: node-fetch: 3.3.2 regenerator-runtime: 0.13.11 - '@floating-ui/core@1.6.9': + '@floating-ui/core@1.7.0': dependencies: '@floating-ui/utils': 0.2.9 '@floating-ui/dom@1.6.13': dependencies: - '@floating-ui/core': 1.6.9 + '@floating-ui/core': 1.7.0 '@floating-ui/utils': 0.2.9 '@floating-ui/utils@0.2.9': {} @@ -5124,32 +5099,32 @@ snapshots: '@gar/promisify@1.1.3': {} - '@ghostery/adblocker-content@2.5.1': + '@ghostery/adblocker-content@2.5.2': dependencies: - '@ghostery/adblocker-extended-selectors': 2.5.1 + '@ghostery/adblocker-extended-selectors': 2.5.2 '@ghostery/adblocker-electron-preload@2.5.1(electron@34.5.3)': dependencies: - '@ghostery/adblocker-content': 2.5.1 + '@ghostery/adblocker-content': 2.5.2 electron: 34.5.3 '@ghostery/adblocker-electron@2.5.1(electron@34.5.3)': dependencies: - '@ghostery/adblocker': 2.5.1 + '@ghostery/adblocker': 2.5.2 '@ghostery/adblocker-electron-preload': 2.5.1(electron@34.5.3) electron: 34.5.3 - tldts-experimental: 6.1.79 + tldts-experimental: 6.1.86 - '@ghostery/adblocker-extended-selectors@2.5.1': {} + '@ghostery/adblocker-extended-selectors@2.5.2': {} - '@ghostery/adblocker@2.5.1': + '@ghostery/adblocker@2.5.2': dependencies: - '@ghostery/adblocker-content': 2.5.1 - '@ghostery/adblocker-extended-selectors': 2.5.1 - '@remusao/guess-url-type': 2.0.0 - '@remusao/small': 2.0.0 - '@remusao/smaz': 2.1.0 - tldts-experimental: 6.1.79 + '@ghostery/adblocker-content': 2.5.2 + '@ghostery/adblocker-extended-selectors': 2.5.2 + '@remusao/guess-url-type': 2.1.0 + '@remusao/small': 2.1.0 + '@remusao/smaz': 2.2.0 + tldts-experimental: 7.0.7 '@hono/node-server@1.14.1(hono@4.7.7)': dependencies: @@ -5182,7 +5157,7 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.2': {} + '@humanwhocodes/retry@0.4.3': {} '@isaacs/cliui@8.0.2': dependencies: @@ -5201,7 +5176,7 @@ snapshots: dependencies: '@nornagon/put': 0.0.8 event-stream: 4.0.1 - jsbi: 4.3.0 + jsbi: 4.3.2 long: 4.0.0 safe-buffer: 5.2.1 xml2js: 0.6.2 @@ -5429,7 +5404,7 @@ snapshots: '@malept/flatpak-bundler@0.4.0(patch_hash=c787371eeb2af011ea934e8818a0dad6d7dcb2df31bbb1686babc7231af0183c)': dependencies: - debug: 4.4.0 + debug: 4.4.1 fs-extra: 9.1.0 lodash: 4.17.21 tmp-promise: 3.0.3 @@ -5438,10 +5413,10 @@ snapshots: '@msgpack/msgpack@2.8.0': {} - '@napi-rs/wasm-runtime@0.2.9': + '@napi-rs/wasm-runtime@0.2.10': dependencies: - '@emnapi/core': 1.4.0 - '@emnapi/runtime': 1.4.0 + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 '@tybys/wasm-util': 0.9.0 optional: true @@ -5486,30 +5461,30 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/core@0.2.0': {} + '@pkgr/core@0.2.4': {} '@playwright/test@1.52.0': dependencies: playwright: 1.52.0 - '@polka/url@1.0.0-next.28': {} + '@polka/url@1.0.0-next.29': {} - '@remusao/guess-url-type@2.0.0': {} + '@remusao/guess-url-type@2.1.0': {} - '@remusao/small@2.0.0': {} + '@remusao/small@2.1.0': {} - '@remusao/smaz-compress@2.1.0': + '@remusao/smaz-compress@2.2.0': dependencies: - '@remusao/trie': 2.0.0 + '@remusao/trie': 2.1.0 - '@remusao/smaz-decompress@2.1.0': {} + '@remusao/smaz-decompress@2.2.0': {} - '@remusao/smaz@2.1.0': + '@remusao/smaz@2.2.0': dependencies: - '@remusao/smaz-compress': 2.1.0 - '@remusao/smaz-decompress': 2.1.0 + '@remusao/smaz-compress': 2.2.0 + '@remusao/smaz-decompress': 2.2.0 - '@remusao/trie@2.0.0': {} + '@remusao/trie@2.1.0': {} '@rollup/rollup-android-arm-eabi@4.40.0': optional: true @@ -5588,16 +5563,16 @@ snapshots: '@skyra/jaro-winkler@1.1.1': {} - '@solid-primitives/refs@1.1.0(solid-js@1.9.5)': + '@solid-primitives/refs@1.1.1(solid-js@1.9.5)': dependencies: - '@solid-primitives/utils': 6.3.0(solid-js@1.9.5) + '@solid-primitives/utils': 6.3.1(solid-js@1.9.5) solid-js: 1.9.5 - '@solid-primitives/transition-group@1.1.0(solid-js@1.9.5)': + '@solid-primitives/transition-group@1.1.1(solid-js@1.9.5)': dependencies: solid-js: 1.9.5 - '@solid-primitives/utils@6.3.0(solid-js@1.9.5)': + '@solid-primitives/utils@6.3.1(solid-js@1.9.5)': dependencies: solid-js: 1.9.5 @@ -5611,14 +5586,6 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tokenizer/inflate@0.2.7': - dependencies: - debug: 4.4.0 - fflate: 0.8.2 - token-types: 6.0.0 - transitivePeerDependencies: - - supports-color - '@tokenizer/token@0.3.0': {} '@tootallnate/once@2.0.0': {} @@ -5638,30 +5605,30 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@types/babel__generator': 7.6.8 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 + '@types/babel__traverse': 7.20.7 - '@types/babel__generator@7.6.8': + '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.27.1 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 - '@types/babel__traverse@7.20.6': + '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.27.1 '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 22.13.5 + '@types/node': 20.17.47 '@types/responselike': 1.0.3 '@types/debug@4.1.12': @@ -5674,17 +5641,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.7 - '@types/json-schema': 7.0.15 - optional: true - '@types/estree@1.0.7': {} '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.15.18 '@types/howler@2.2.12': {} @@ -5698,31 +5659,31 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 22.13.5 + '@types/node': 20.17.47 '@types/ms@2.1.0': {} '@types/node@16.9.1': {} - '@types/node@20.17.31': + '@types/node@20.17.47': dependencies: undici-types: 6.19.8 - '@types/node@22.13.5': + '@types/node@22.15.18': dependencies: - undici-types: 6.20.0 + undici-types: 6.21.0 '@types/parse-json@4.0.2': {} '@types/plist@3.0.5': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.15.18 xmlbuilder: 15.1.1 optional: true '@types/responselike@1.0.3': dependencies: - '@types/node': 22.13.5 + '@types/node': 20.17.47 '@types/semver@7.7.0': {} @@ -5733,7 +5694,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.13.5 + '@types/node': 20.17.47 optional: true '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3)': @@ -5748,7 +5709,7 @@ snapshots: graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.3) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5759,7 +5720,7 @@ snapshots: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0 + debug: 4.4.1 eslint: 9.25.1 typescript: 5.8.3 transitivePeerDependencies: @@ -5774,9 +5735,9 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - debug: 4.4.0 + debug: 4.4.1 eslint: 9.25.1 - ts-api-utils: 2.0.1(typescript@5.8.3) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5787,19 +5748,19 @@ snapshots: dependencies: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0 + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.8.3) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.25.1) '@typescript-eslint/scope-manager': 8.31.0 '@typescript-eslint/types': 8.31.0 '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) @@ -5813,61 +5774,64 @@ snapshots: '@typescript-eslint/types': 8.31.0 eslint-visitor-keys: 4.2.0 - '@unrs/resolver-binding-darwin-arm64@1.6.3': + '@unrs/resolver-binding-darwin-arm64@1.7.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.7.2': optional: true - '@unrs/resolver-binding-darwin-x64@1.6.3': + '@unrs/resolver-binding-freebsd-x64@1.7.2': optional: true - '@unrs/resolver-binding-freebsd-x64@1.6.3': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.6.3': + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.6.3': + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.6.3': + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.6.3': + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.6.3': + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.6.3': + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.6.3': + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.6.3': + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.6.3': + '@unrs/resolver-binding-linux-x64-musl@1.7.2': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.6.3': + '@unrs/resolver-binding-wasm32-wasi@1.7.2': dependencies: - '@napi-rs/wasm-runtime': 0.2.9 + '@napi-rs/wasm-runtime': 0.2.10 optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.6.3': + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.6.3': + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.6.3': + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': optional: true '@vladfrangu/async_event_emitter@2.4.6': {} '@xhayper/discord-rpc@1.2.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: - '@discordjs/rest': 2.4.3 + '@discordjs/rest': 2.5.0 '@vladfrangu/async_event_emitter': 2.4.6 discord-api-types: 0.37.120 ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -5881,7 +5845,7 @@ snapshots: abbrev@1.1.1: {} - abbrev@3.0.0: {} + abbrev@3.0.1: {} abort-controller@3.0.0: dependencies: @@ -5892,15 +5856,15 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.14.0 + acorn: 8.14.1 - acorn@8.14.0: {} + acorn@8.14.1: {} agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -5980,9 +5944,9 @@ snapshots: builder-util-runtime: 9.3.1 chromium-pickle-js: 0.2.0 config-file-ts: 0.2.8-rc1 - debug: 4.4.0 + debug: 4.4.1 dmg-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12) - dotenv: 16.4.7 + dotenv: 16.5.0 dotenv-expand: 11.0.7 ejs: 3.1.10 electron-builder-squirrel-windows: 26.0.12(dmg-builder@26.0.12) @@ -6013,7 +5977,7 @@ snapshots: array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 array-includes@3.1.8: @@ -6025,9 +5989,10 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -6093,14 +6058,14 @@ snapshots: await-to-js@3.0.0: {} - babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.10): + babel-plugin-jsx-dom-expressions@0.39.8(@babel/core@7.27.1): dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.1 '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.27.0 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) + '@babel/types': 7.27.1 html-entities: 2.3.3 - parse5: 7.2.1 + parse5: 7.3.0 validate-html-nesting: 1.2.2 babel-plugin-macros@2.8.0: @@ -6115,10 +6080,10 @@ snapshots: babel-plugin-macros: 2.8.0 require-from-string: 2.0.2 - babel-preset-solid@1.9.5(@babel/core@7.26.10): + babel-preset-solid@1.9.6(@babel/core@7.27.1): dependencies: - '@babel/core': 7.26.10 - babel-plugin-jsx-dom-expressions: 0.39.7(@babel/core@7.26.10) + '@babel/core': 7.27.1 + babel-plugin-jsx-dom-expressions: 0.39.8(@babel/core@7.27.1) balanced-match@1.0.2: {} @@ -6126,7 +6091,7 @@ snapshots: bgutils-js@3.2.0: {} - birpc@2.2.0: {} + birpc@2.3.0: {} bl@4.1.0: dependencies: @@ -6169,12 +6134,12 @@ snapshots: dependencies: fancy-regex: 0.5.4 - browserslist@4.24.4: + browserslist@4.24.5: dependencies: - caniuse-lite: 1.0.30001701 - electron-to-chromium: 1.5.109 + caniuse-lite: 1.0.30001718 + electron-to-chromium: 1.5.155 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.4) + update-browserslist-db: 1.1.3(browserslist@4.24.5) buffer-crc32@0.2.13: {} @@ -6196,7 +6161,7 @@ snapshots: builder-util-runtime@9.3.1: dependencies: - debug: 4.4.0 + debug: 4.4.1 sax: 1.4.1 transitivePeerDependencies: - supports-color @@ -6209,7 +6174,7 @@ snapshots: builder-util-runtime: 9.3.1 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1 fs-extra: 10.1.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -6288,7 +6253,7 @@ snapshots: dependencies: clone-response: 1.0.3 get-stream: 5.2.0 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 keyv: 4.5.4 lowercase-keys: 2.0.0 normalize-url: 6.1.0 @@ -6306,7 +6271,7 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 @@ -6315,7 +6280,7 @@ snapshots: camelcase@7.0.1: {} - caniuse-lite@1.0.30001701: {} + caniuse-lite@1.0.30001718: {} chalk-template@0.4.0: dependencies: @@ -6376,7 +6341,7 @@ snapshots: dependencies: color-name: 1.1.4 - color-convert@3.0.1: + color-convert@3.1.0: dependencies: color-name: 2.0.0 @@ -6390,7 +6355,7 @@ snapshots: color@5.0.0: dependencies: - color-convert: 3.0.1 + color-convert: 3.1.0 color-string: 2.0.1 combined-stream@1.0.8: @@ -6410,7 +6375,7 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.53.0 + mime-db: 1.54.0 compression@1.7.4: dependencies: @@ -6498,19 +6463,19 @@ snapshots: data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-offset@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 @@ -6526,7 +6491,7 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: + debug@4.4.1: dependencies: ms: 2.1.3 @@ -6553,7 +6518,7 @@ snapshots: side-channel: 1.1.0 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 deep-extend@0.6.0: {} @@ -6608,7 +6573,7 @@ snapshots: delayed-stream@1.0.0: {} - detect-libc@2.0.3: {} + detect-libc@2.0.4: {} detect-node@2.1.0: optional: true @@ -6673,13 +6638,13 @@ snapshots: dot-prop@9.0.0: dependencies: - type-fest: 4.35.0 + type-fest: 4.41.0 dotenv-expand@11.0.7: dependencies: - dotenv: 16.4.7 + dotenv: 16.5.0 - dotenv@16.4.7: {} + dotenv@16.5.0: {} doublearray@0.0.2: {} @@ -6754,7 +6719,7 @@ snapshots: electron-localshortcut@3.2.1: dependencies: - debug: 4.4.0 + debug: 4.4.1 electron-is-accelerator: 0.1.2 keyboardevent-from-electron-accelerator: 2.0.0 keyboardevents-areequal: 0.2.2 @@ -6777,9 +6742,9 @@ snapshots: electron-store@10.0.1: dependencies: conf: 13.1.0 - type-fest: 4.35.0 + type-fest: 4.41.0 - electron-to-chromium@1.5.109: {} + electron-to-chromium@1.5.155: {} electron-unhandled@4.0.1: dependencies: @@ -6802,34 +6767,34 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@3.1.0(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + electron-vite@3.1.0(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.10) + '@babel/core': 7.27.1 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.1) cac: 6.7.14 esbuild: 0.25.3 magic-string: 0.30.17 picocolors: 1.1.1 - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) transitivePeerDependencies: - supports-color electron-winstaller@5.4.0: dependencies: - '@electron/asar': 3.3.1 - debug: 4.4.0 + '@electron/asar': 3.4.1 + debug: 4.4.1 fs-extra: 7.0.1 lodash: 4.17.21 temp: 0.9.4 optionalDependencies: - '@electron/windows-sign': 1.2.1 + '@electron/windows-sign': 1.2.2 transitivePeerDependencies: - supports-color electron@34.5.3: dependencies: '@electron/get': 2.0.3 - '@types/node': 20.17.31 + '@types/node': 20.17.47 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -6851,6 +6816,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -6869,7 +6836,7 @@ snapshots: arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 @@ -6915,7 +6882,7 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 es-define-property@1.0.1: {} @@ -7011,13 +6978,13 @@ snapshots: eslint-import-resolver-typescript@4.3.4(eslint-plugin-import@2.31.0)(eslint@9.25.1): dependencies: - debug: 4.4.0 + debug: 4.4.1 eslint: 9.25.1 get-tsconfig: 4.10.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.13 - unrs-resolver: 1.6.3 + unrs-resolver: 1.7.2 optionalDependencies: eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.25.1) transitivePeerDependencies: @@ -7038,7 +7005,7 @@ snapshots: dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 @@ -7063,14 +7030,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.2): + eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1))(eslint@9.25.1)(prettier@3.5.3): dependencies: eslint: 9.25.1 - prettier: 3.5.2 + prettier: 3.5.3 prettier-linter-helpers: 1.0.0 - synckit: 0.11.1 + synckit: 0.11.6 optionalDependencies: - '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.2(eslint@9.25.1) eslint-scope@8.3.0: @@ -7084,23 +7050,23 @@ snapshots: eslint@9.25.1: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.25.1) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 + '@eslint/config-helpers': 0.2.2 '@eslint/core': 0.13.0 '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.25.1 '@eslint/plugin-kit': 0.2.8 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 + '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1 escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -7124,8 +7090,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 esquery@1.6.0: @@ -7174,7 +7140,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.1 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -7226,12 +7192,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - fetch-blob@4.0.0: - dependencies: - node-domexception: 1.0.0 - - fflate@0.8.2: {} - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7242,15 +7202,6 @@ snapshots: strtok3: 6.3.0 token-types: 4.2.1 - file-type@20.5.0: - dependencies: - '@tokenizer/inflate': 0.2.7 - strtok3: 10.2.2 - token-types: 6.0.0 - uint8array-extras: 1.4.0 - transitivePeerDependencies: - - supports-color - filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -7351,7 +7302,7 @@ snapshots: function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 hasown: 2.0.2 @@ -7389,7 +7340,7 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 @@ -7468,7 +7419,7 @@ snapshots: dependencies: '@sindresorhus/merge-streams': 2.3.0 fast-glob: 3.3.3 - ignore: 7.0.3 + ignore: 7.0.4 path-type: 6.0.0 slash: 5.1.0 unicorn-magic: 0.3.0 @@ -7553,20 +7504,20 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.2.0: {} http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -7578,14 +7529,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -7619,7 +7570,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.3: {} + ignore@7.0.4: {} image-q@4.0.0: dependencies: @@ -7660,13 +7611,13 @@ snapshots: is-arguments@1.2.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-arrayish@0.2.1: {} @@ -7674,7 +7625,7 @@ snapshots: is-async-function@2.1.1: dependencies: async-function: 1.0.0 - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -7685,7 +7636,7 @@ snapshots: is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-bun-module@2.0.0: @@ -7704,13 +7655,13 @@ snapshots: is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-docker@2.2.1: {} @@ -7721,13 +7672,13 @@ snapshots: is-finalizationregistry@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -7748,7 +7699,7 @@ snapshots: is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} @@ -7761,7 +7712,7 @@ snapshots: is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -7770,24 +7721,24 @@ snapshots: is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-stream@2.0.1: {} is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 is-unicode-supported@0.1.0: {} @@ -7797,11 +7748,11 @@ snapshots: is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-what@4.1.16: {} @@ -7875,7 +7826,7 @@ snapshots: jintr@3.3.1: dependencies: - acorn: 8.14.0 + acorn: 8.14.1 jpeg-js@0.4.4: {} @@ -7885,7 +7836,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsbi@4.3.0: {} + jsbi@4.3.2: {} jsbn@1.1.0: {} @@ -7997,7 +7948,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} + lru-cache@11.1.0: {} lru-cache@5.1.1: dependencies: @@ -8009,7 +7960,7 @@ snapshots: lru-cache@7.18.3: {} - magic-bytes.js@1.10.0: {} + magic-bytes.js@1.12.1: {} magic-string@0.30.17: dependencies: @@ -8019,7 +7970,7 @@ snapshots: dependencies: agentkeepalive: 4.6.0 cacache: 16.1.3 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-lambda: 1.0.1 @@ -8041,7 +7992,7 @@ snapshots: dependencies: '@npmcli/agent': 3.0.0 cacache: 19.0.1 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 minipass: 7.1.2 minipass-fetch: 4.0.1 minipass-flush: 1.0.5 @@ -8081,7 +8032,7 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.53.0: {} + mime-db@1.54.0: {} mime-types@2.1.18: dependencies: @@ -8141,7 +8092,7 @@ snapshots: dependencies: minipass: 7.1.2 minipass-sized: 1.0.3 - minizlib: 3.0.1 + minizlib: 3.0.2 optionalDependencies: encoding: 0.1.13 @@ -8170,10 +8121,9 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - minizlib@3.0.1: + minizlib@3.0.2: dependencies: minipass: 7.1.2 - rimraf: 5.0.10 mkdirp@0.5.6: dependencies: @@ -8189,9 +8139,9 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} - napi-postinstall@0.1.5: {} + napi-postinstall@0.2.4: {} natural-compare@1.4.0: {} @@ -8201,14 +8151,14 @@ snapshots: negotiator@1.0.0: {} - node-abi@3.74.0: + node-abi@3.75.0: dependencies: semver: 7.7.1 node-addon-api@1.7.2: optional: true - node-api-version@0.2.0: + node-api-version@0.2.1: dependencies: semver: 7.7.1 @@ -8254,7 +8204,7 @@ snapshots: nopt@8.1.0: dependencies: - abbrev: 3.0.0 + abbrev: 3.0.1 normalize-url@6.1.0: {} @@ -8278,7 +8228,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -8300,7 +8250,7 @@ snapshots: object.values@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -8318,7 +8268,7 @@ snapshots: dependencies: mimic-fn: 2.1.0 - open@10.1.0: + open@10.1.2: dependencies: default-browser: 5.2.1 define-lazy-prop: 3.0.0 @@ -8327,7 +8277,7 @@ snapshots: openapi3-ts@4.4.0: dependencies: - yaml: 2.7.0 + yaml: 2.8.0 optionator@0.9.4: dependencies: @@ -8393,14 +8343,14 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse5@7.2.1: + parse5@7.3.0: dependencies: - entities: 4.5.0 + entities: 6.0.0 parseley@0.12.1: dependencies: @@ -8426,7 +8376,7 @@ snapshots: path-scurry@2.0.0: dependencies: - lru-cache: 11.0.2 + lru-cache: 11.1.0 minipass: 7.1.2 path-to-regexp@3.3.0: {} @@ -8447,8 +8397,6 @@ snapshots: peek-readable@4.1.0: {} - peek-readable@7.0.0: {} - peerjs-js-binarypack@2.1.0: {} peerjs@1.5.4: @@ -8456,7 +8404,7 @@ snapshots: '@msgpack/msgpack': 2.8.0 eventemitter3: 4.0.7 peerjs-js-binarypack: 2.1.0 - webrtc-adapter: 9.0.1 + webrtc-adapter: 9.0.3 pend@1.2.0: {} @@ -8500,7 +8448,7 @@ snapshots: postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -8515,7 +8463,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.5.2: {} + prettier@3.5.3: {} preval.macro@4.0.0: dependencies: @@ -8560,7 +8508,7 @@ snapshots: read-binary-file-arch@1.0.6: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -8668,10 +8616,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@5.0.10: - dependencies: - glob: 10.4.5 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -8717,7 +8661,7 @@ snapshots: safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -8733,7 +8677,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -8773,11 +8717,11 @@ snapshots: dependencies: type-fest: 0.20.2 - seroval-plugins@1.2.1(seroval@1.2.1): + seroval-plugins@1.3.1(seroval@1.3.1): dependencies: - seroval: 1.2.1 + seroval: 1.3.1 - seroval@1.2.1: {} + seroval@1.3.1: {} serve-handler@6.1.6: dependencies: @@ -8842,14 +8786,14 @@ snapshots: side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 @@ -8877,7 +8821,7 @@ snapshots: sirv@3.0.1: dependencies: - '@polka/url': 1.0.0-next.28 + '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 @@ -8895,7 +8839,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -8903,7 +8847,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.1 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -8921,14 +8865,14 @@ snapshots: solid-js@1.9.5: dependencies: csstype: 3.1.3 - seroval: 1.2.1 - seroval-plugins: 1.2.1(seroval@1.2.1) + seroval: 1.3.1 + seroval-plugins: 1.3.1(seroval@1.3.1) solid-refresh@0.6.3(solid-js@1.9.5): dependencies: - '@babel/generator': 7.27.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/types': 7.27.0 + '@babel/generator': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/types': 7.27.1 solid-js: 1.9.5 transitivePeerDependencies: - supports-color @@ -8941,8 +8885,8 @@ snapshots: solid-transition-group@0.3.0(solid-js@1.9.5): dependencies: - '@solid-primitives/refs': 1.1.0(solid-js@1.9.5) - '@solid-primitives/transition-group': 1.1.0(solid-js@1.9.5) + '@solid-primitives/refs': 1.1.1(solid-js@1.9.5) + '@solid-primitives/transition-group': 1.1.1(solid-js@1.9.5) solid-js: 1.9.5 source-map-js@1.2.1: {} @@ -8997,7 +8941,7 @@ snapshots: string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.23.9 @@ -9007,7 +8951,7 @@ snapshots: string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -9041,11 +8985,6 @@ snapshots: strip-json-comments@3.1.1: {} - strtok3@10.2.2: - dependencies: - '@tokenizer/token': 0.3.0 - peek-readable: 7.0.0 - strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 @@ -9055,7 +8994,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -9065,10 +9004,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.11.1: + synckit@0.11.6: dependencies: - '@pkgr/core': 0.2.0 - tslib: 2.8.1 + '@pkgr/core': 0.2.4 tar@6.2.1: dependencies: @@ -9084,7 +9022,7 @@ snapshots: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 minipass: 7.1.2 - minizlib: 3.0.1 + minizlib: 3.0.2 mkdirp: 3.0.1 yallist: 5.0.0 @@ -9113,11 +9051,17 @@ snapshots: fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 - tldts-core@6.1.79: {} + tldts-core@6.1.86: {} + + tldts-core@7.0.7: {} - tldts-experimental@6.1.79: + tldts-experimental@6.1.86: dependencies: - tldts-core: 6.1.79 + tldts-core: 6.1.86 + + tldts-experimental@7.0.7: + dependencies: + tldts-core: 7.0.7 tmp-promise@3.0.3: dependencies: @@ -9134,18 +9078,13 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - token-types@6.0.0: - dependencies: - '@tokenizer/token': 0.3.0 - ieee754: 1.2.1 - totalist@3.0.1: {} truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.5 - ts-api-utils@2.0.1(typescript@5.8.3): + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -9174,11 +9113,11 @@ snapshots: type-fest@2.19.0: {} - type-fest@4.35.0: {} + type-fest@4.41.0: {} typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 @@ -9225,16 +9164,16 @@ snapshots: unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 undici-types@6.19.8: {} - undici-types@6.20.0: {} + undici-types@6.21.0: {} - undici@5.28.5: + undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 @@ -9267,26 +9206,27 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.2 - unrs-resolver@1.6.3: + unrs-resolver@1.7.2: dependencies: - napi-postinstall: 0.1.5 + napi-postinstall: 0.2.4 optionalDependencies: - '@unrs/resolver-binding-darwin-arm64': 1.6.3 - '@unrs/resolver-binding-darwin-x64': 1.6.3 - '@unrs/resolver-binding-freebsd-x64': 1.6.3 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.6.3 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.6.3 - '@unrs/resolver-binding-linux-arm64-gnu': 1.6.3 - '@unrs/resolver-binding-linux-arm64-musl': 1.6.3 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.6.3 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.6.3 - '@unrs/resolver-binding-linux-s390x-gnu': 1.6.3 - '@unrs/resolver-binding-linux-x64-gnu': 1.6.3 - '@unrs/resolver-binding-linux-x64-musl': 1.6.3 - '@unrs/resolver-binding-wasm32-wasi': 1.6.3 - '@unrs/resolver-binding-win32-arm64-msvc': 1.6.3 - '@unrs/resolver-binding-win32-ia32-msvc': 1.6.3 - '@unrs/resolver-binding-win32-x64-msvc': 1.6.3 + '@unrs/resolver-binding-darwin-arm64': 1.7.2 + '@unrs/resolver-binding-darwin-x64': 1.7.2 + '@unrs/resolver-binding-freebsd-x64': 1.7.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.7.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.7.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-musl': 1.7.2 + '@unrs/resolver-binding-wasm32-wasi': 1.7.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.7.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.7.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.7.2 unzip-crx-3@0.2.0: dependencies: @@ -9294,9 +9234,9 @@ snapshots: mkdirp: 0.5.6 yaku: 0.16.7 - update-browserslist-db@1.1.3(browserslist@4.24.4): + update-browserslist-db@1.1.3(browserslist@4.24.5): dependencies: - browserslist: 4.24.4 + browserslist: 4.24.5 escalade: 3.2.0 picocolors: 1.1.1 @@ -9334,28 +9274,28 @@ snapshots: extsprintf: 1.4.1 optional: true - vite-dev-rpc@1.0.7(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + vite-dev-rpc@1.0.7(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): dependencies: - birpc: 2.2.0 - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) - vite-hot-client: 2.0.4(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + birpc: 2.3.0 + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) + vite-hot-client: 2.0.4(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) - vite-hot-client@2.0.4(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + vite-hot-client@2.0.4(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): dependencies: - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) - vite-plugin-inspect@11.0.1(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + vite-plugin-inspect@11.0.1(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): dependencies: ansis: 3.17.0 - debug: 4.4.0 + debug: 4.4.1 error-stack-parser-es: 1.0.5 ohash: 2.0.11 - open: 10.1.0 + open: 10.1.2 perfect-debounce: 1.0.0 sirv: 3.0.1 unplugin-utils: 0.2.4 - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) - vite-dev-rpc: 1.0.7(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) + vite-dev-rpc: 1.0.7(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) transitivePeerDependencies: - supports-color @@ -9363,20 +9303,20 @@ snapshots: dependencies: lib-esm: 0.4.2 - vite-plugin-solid@2.11.6(solid-js@1.9.5)(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + vite-plugin-solid@2.11.6(solid-js@1.9.5)(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): dependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.27.1 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.5(@babel/core@7.26.10) + babel-preset-solid: 1.9.6(@babel/core@7.27.1) merge-anything: 5.1.7 solid-js: 1.9.5 solid-refresh: 0.6.3(solid-js@1.9.5) - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) - vitefu: 1.0.6(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)) + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) + vitefu: 1.0.6(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) transitivePeerDependencies: - supports-color - vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0): + vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0): dependencies: esbuild: 0.25.3 fdir: 6.4.4(picomatch@4.0.2) @@ -9385,13 +9325,13 @@ snapshots: rollup: 4.40.0 tinyglobby: 0.2.13 optionalDependencies: - '@types/node': 22.13.5 + '@types/node': 22.15.18 fsevents: 2.3.3 - yaml: 2.7.0 + yaml: 2.8.0 - vitefu@1.0.6(vite@6.3.3(@types/node@22.13.5)(yaml@2.7.0)): + vitefu@1.0.6(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)): optionalDependencies: - vite: 6.3.3(@types/node@22.13.5)(yaml@2.7.0) + vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) vudio@2.1.1(patch_hash=0e06c2ed11c02bdc490c209fa80070e98517c2735c641f5738b6e15d7dc1959d): {} @@ -9403,7 +9343,7 @@ snapshots: webidl-conversions@7.0.0: {} - webrtc-adapter@9.0.1: + webrtc-adapter@9.0.3: dependencies: sdp: 3.2.0 @@ -9421,7 +9361,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.1.1 @@ -9433,7 +9373,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -9442,12 +9382,13 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 for-each: 0.3.5 + get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 @@ -9511,7 +9452,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.7.0: {} + yaml@2.8.0: {} yargs-parser@21.1.1: {} @@ -9534,10 +9475,10 @@ snapshots: youtubei.js@13.4.0: dependencies: - '@bufbuild/protobuf': 2.2.3 + '@bufbuild/protobuf': 2.4.0 jintr: 3.3.1 tslib: 2.8.1 - undici: 5.28.5 + undici: 5.29.0 zlibjs@0.3.1: {} From 5d4c531ed9eb5ae76bb207c2f4e8e322452e3b26 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sat, 17 May 2025 15:07:14 -0600 Subject: [PATCH 25/29] refactor: simplify image file creation by removing Blob fallback --- src/plugins/slack-now-playing/main.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index 4927d9dc5b..a498cad813 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -346,14 +346,7 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon // Read the file as a Buffer and append directly to FormData const fileBuffer = await fs.promises.readFile(filePath); const filename = path.basename(filePath) || 'emoji.png'; - let imageFile; - if (typeof File !== 'undefined') { - imageFile = new File([fileBuffer], filename); - } else if (typeof Blob !== 'undefined') { - imageFile = new Blob([fileBuffer]); - } else { - throw new Error('Neither File nor Blob is available in this environment'); - } + const imageFile = new File([fileBuffer], filename); formData.append('image', imageFile, filename); } catch (fileError: any) { From 97e928ed59420871b6a407547d8a629ebc641bce Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sun, 18 May 2025 15:01:28 -0600 Subject: [PATCH 26/29] (i18n) Revert changes to most language files --- src/i18n/resources/ar.json | 11 -------- src/i18n/resources/bg.json | 12 --------- src/i18n/resources/bs.json | 13 ---------- src/i18n/resources/ca.json | 11 -------- src/i18n/resources/cs.json | 11 -------- src/i18n/resources/da.json | 11 -------- src/i18n/resources/de.json | 33 ++++++++++++++++-------- src/i18n/resources/el.json | 35 +++++++++++++++++-------- src/i18n/resources/et.json | 11 -------- src/i18n/resources/fa.json | 11 -------- src/i18n/resources/fi.json | 11 -------- src/i18n/resources/fil.json | 11 -------- src/i18n/resources/fr.json | 11 -------- src/i18n/resources/he.json | 11 -------- src/i18n/resources/hi.json | 11 -------- src/i18n/resources/hr.json | 11 -------- src/i18n/resources/hu.json | 11 -------- src/i18n/resources/id.json | 11 -------- src/i18n/resources/is.json | 11 -------- src/i18n/resources/it.json | 11 -------- src/i18n/resources/ja.json | 25 ++++++++++-------- src/i18n/resources/ka.json | 11 -------- src/i18n/resources/kn.json | 14 ---------- src/i18n/resources/ko.json | 11 -------- src/i18n/resources/lt.json | 11 -------- src/i18n/resources/ml.json | 13 ---------- src/i18n/resources/ms.json | 27 ++++++++++---------- src/i18n/resources/nb.json | 11 -------- src/i18n/resources/ne.json | 48 +++++++++++++++++++++++++++-------- src/i18n/resources/nl.json | 11 -------- src/i18n/resources/pl.json | 11 -------- src/i18n/resources/pt-BR.json | 35 +++++++++++++++++-------- src/i18n/resources/pt.json | 11 -------- src/i18n/resources/ro.json | 11 -------- src/i18n/resources/ru.json | 35 +++++++++++++++++-------- src/i18n/resources/si.json | 13 ---------- src/i18n/resources/sl.json | 11 -------- src/i18n/resources/sr.json | 14 ---------- src/i18n/resources/sv.json | 11 -------- src/i18n/resources/ta.json | 11 -------- src/i18n/resources/th.json | 11 -------- src/i18n/resources/tr.json | 39 ++++++++++++++++++++-------- src/i18n/resources/uk.json | 11 -------- src/i18n/resources/ur.json | 13 ---------- src/i18n/resources/vi.json | 11 -------- src/i18n/resources/zh-CN.json | 35 +++++++++++++++++-------- src/i18n/resources/zh-TW.json | 35 +++++++++++++++++-------- 47 files changed, 235 insertions(+), 534 deletions(-) diff --git a/src/i18n/resources/ar.json b/src/i18n/resources/ar.json index 1324dcc032..be3714bbbf 100644 --- a/src/i18n/resources/ar.json +++ b/src/i18n/resources/ar.json @@ -840,17 +840,6 @@ "visualizer-type": "نوع المعاينة المصرية" }, "name": "معاين بصري" - }, - "slack-now-playing": { - "name": "حالة سلاك", - "description": "يضبط حالة سلاك إلى الأغنية التي تعمل حاليًا", - "status-text": "يشغل الآن: {{artist}} - {{title}}", - "menu": { - "settings": "الإعدادات", - "token": "رمز API سلاك", - "cookie-token": "رمز كوكي سلاك", - "emoji-name": "اسم الرمز التعبيري المخصص" - } } } } diff --git a/src/i18n/resources/bg.json b/src/i18n/resources/bg.json index d231d4a77f..bbc3fe97b5 100644 --- a/src/i18n/resources/bg.json +++ b/src/i18n/resources/bg.json @@ -670,17 +670,6 @@ "description": "Позволява промяна на качеството на видеото с бутон върху видеото", "name": "Промяна на качеството на видеото" }, - "slack-now-playing": { - "name": "Статус в Slack", - "description": "Задава вашия статус в Slack на текущо възпроизвежданата песен", - "status-text": "Сега свири: {{artist}} - {{title}}", - "menu": { - "settings": "Настройки", - "token": "Slack API Токен", - "cookie-token": "Slack Cookie Токен", - "emoji-name": "Име на персонализиран emoji" - } - }, "scrobbler": { "description": "Добавяне на скробблинг поддръжка (last.fm, Listenbrainz и т.н.)", "dialog": { @@ -858,4 +847,3 @@ } } } - diff --git a/src/i18n/resources/bs.json b/src/i18n/resources/bs.json index d091576d0f..ee36f8d23b 100644 --- a/src/i18n/resources/bs.json +++ b/src/i18n/resources/bs.json @@ -129,18 +129,5 @@ } } } - }, - "plugins": { - "slack-now-playing": { - "name": "Slack status", - "description": "Postavlja vaš Slack status na pjesmu koja se trenutno reproducira", - "status-text": "Sada svira: {{artist}} - {{title}}", - "menu": { - "settings": "Postavke", - "token": "Slack API token", - "cookie-token": "Slack Cookie token", - "emoji-name": "Naziv prilagođenog emoji-ja" - } - } } } diff --git a/src/i18n/resources/ca.json b/src/i18n/resources/ca.json index e3609559e7..90504e741a 100644 --- a/src/i18n/resources/ca.json +++ b/src/i18n/resources/ca.json @@ -808,17 +808,6 @@ "visualizer-type": "Tipus de visualitzador" }, "name": "Visualitzador" - }, - "slack-now-playing": { - "name": "Estat de Slack", - "description": "Estableix el teu estat de Slack a la cançó que s'està reproduint actualment", - "status-text": "Ara sonant: {{artist}} - {{title}}", - "menu": { - "settings": "Configuració", - "token": "Token API de Slack", - "cookie-token": "Token de Cookie de Slack", - "emoji-name": "Nom d'emoji personalitzat" - } } } } diff --git a/src/i18n/resources/cs.json b/src/i18n/resources/cs.json index a1f1ef13aa..b2786e678a 100644 --- a/src/i18n/resources/cs.json +++ b/src/i18n/resources/cs.json @@ -774,17 +774,6 @@ "visualizer-type": "Typ vizualizéru" }, "name": "Vizualizér" - }, - "slack-now-playing": { - "name": "Slack status", - "description": "Nastaví váš Slack status na aktuálně přehrávanou píseň", - "status-text": "Nyní hraje: {{artist}} - {{title}}", - "menu": { - "settings": "Nastavení", - "token": "Slack API token", - "cookie-token": "Slack Cookie token", - "emoji-name": "Název vlastního emoji" - } } } } diff --git a/src/i18n/resources/da.json b/src/i18n/resources/da.json index 5c73a803c6..729e96493f 100644 --- a/src/i18n/resources/da.json +++ b/src/i18n/resources/da.json @@ -310,17 +310,6 @@ "advanced": "Avanceret" }, "name": "Fade [Beta]" - }, - "slack-now-playing": { - "name": "Slack-status", - "description": "Sætter din Slack-status til den sang, der afspilles i øjeblikket", - "status-text": "Spiller nu: {{artist}} - {{title}}", - "menu": { - "settings": "Indstillinger", - "token": "Slack API-token", - "cookie-token": "Slack Cookie-token", - "emoji-name": "Brugerdefineret emoji-navn" - } } } } diff --git a/src/i18n/resources/de.json b/src/i18n/resources/de.json index 3f177ac96c..52a9be30c7 100644 --- a/src/i18n/resources/de.json +++ b/src/i18n/resources/de.json @@ -333,6 +333,28 @@ "description": "Kompressor auf Audio anwenden (senkt die Lautstärke der lautesten Teile des Signals und hebt die Lautstärke der leisesten Teile an)", "name": "Audio-Komprimierer" }, + "auth-proxy-adapter": { + "description": "Unterstützung für Proxy-Authentifizierungsdienste", + "menu": { + "disable": "Proxy-Adapter deaktivieren", + "enable": "Proxy-Adapter aktivieren", + "hostname": { + "label": "Hostname" + }, + "port": { + "label": "Port" + } + }, + "prompt": { + "hostname": { + "label": "Hostnamen eingeben für lokalen Proxy-Server (Neustart erforderlich):", + "title": "Proxy Hostname" + }, + "port": { + "title": "Proxy Port" + } + } + }, "blur-nav-bar": { "description": "Macht Navigationsleiste durchsichtig und unscharf", "name": "Verschwommene Navigationsleiste" @@ -392,17 +414,6 @@ }, "name": "Deaktiviere automatisches Abspielen" }, - "slack-now-playing": { - "description": "Setzt deinen Slack-Status auf den aktuell abgespielten Song", - "menu": { - "settings": "Einstellungen", - "token": "Slack API-Token", - "cookie-token": "Slack Cookie-Token", - "emoji-name": "Benutzerdefinierter Emoji-Name" - }, - "name": "Slack Status", - "status-text": "Jetzt spielt: {{artist}} - {{title}}" - }, "discord": { "backend": { "already-connected": "Verbindungsaufbau bei aktiver Verbindung versucht", diff --git a/src/i18n/resources/el.json b/src/i18n/resources/el.json index 525cbca31f..79e4f35d38 100644 --- a/src/i18n/resources/el.json +++ b/src/i18n/resources/el.json @@ -333,6 +333,30 @@ "description": "Συμπίεση ήχου (μειώνει την ένταση των πιο δυνατών τμημάτων του κύματος και αυξάνει την ένταση των πιο μαλακών τμημάτων)", "name": "Συμπιεστής ήχου" }, + "auth-proxy-adapter": { + "description": "Υποστήριξη για τη χρήση υπηρεσιών μεσολάβησης αυθεντικοποίησης", + "menu": { + "disable": "Απενεργοποίηση προσαρμογέα μεσολάβησης", + "enable": "Ενεργοποίηση προσαρμογέα μεσολάβησης", + "hostname": { + "label": "Όνομα οικοδεσπότη" + }, + "port": { + "label": "Θύρα" + } + }, + "name": "Προσαρμογέας μεσολάβησης Auth", + "prompt": { + "hostname": { + "label": "Εισάγετε το όνομα κεντρικού υπολογιστή για τον τοπικό διακομιστή μεσολάβησης (απαιτείται επανεκκίνηση):", + "title": "Όνομα κεντρικού υπολογιστή μεσολάβησης" + }, + "port": { + "label": "Εισάγετε τη θύρα για τον τοπικό διακομιστή μεσολάβησης (απαιτεί επανεκκίνηση):", + "title": "Θύρα διακομιστή μεσολάβησης" + } + } + }, "blur-nav-bar": { "description": "θέτει τη γραμμή πλοήγησης διαφανή και θολή", "name": "Θόλωμα γραμμής πλοήγησης" @@ -838,17 +862,6 @@ "button": "Τραγούδι" } }, - "slack-now-playing": { - "name": "Κατάσταση Slack", - "description": "Ορίζει την κατάσταση του Slack στο τραγούδι που παίζει αυτή τη στιγμή", - "status-text": "Παίζει τώρα: {{artist}} - {{title}}", - "menu": { - "settings": "Ρυθμίσεις", - "token": "Διακριτικό API Slack", - "cookie-token": "Διακριτικό Cookie Slack", - "emoji-name": "Όνομα προσαρμοσμένου emoji" - } - }, "visualizer": { "description": "Προσθέτει έναν απεικονιστή στο πρόγραμμα αναπαραγωγής", "menu": { diff --git a/src/i18n/resources/et.json b/src/i18n/resources/et.json index 31f5c4dfcf..9c471c2823 100644 --- a/src/i18n/resources/et.json +++ b/src/i18n/resources/et.json @@ -219,17 +219,6 @@ }, "tuna-obs": { "description": "Lõimimine OBSi Tuna lisamooduliga" - }, - "slack-now-playing": { - "name": "Slacki olek", - "description": "Seadistab sinu Slacki oleku hetkel mängiva laulu järgi", - "status-text": "Praegu mängib: {{artist}} - {{title}}", - "menu": { - "settings": "Seaded", - "token": "Slacki API võti", - "cookie-token": "Slacki küpsise võti", - "emoji-name": "Kohandatud emoji nimi" - } } } } diff --git a/src/i18n/resources/fa.json b/src/i18n/resources/fa.json index 544c793741..b7253df413 100644 --- a/src/i18n/resources/fa.json +++ b/src/i18n/resources/fa.json @@ -840,17 +840,6 @@ "visualizer-type": "نوع نمایش‌دهنده تصویری" }, "name": "نمایش‌دهنده تصویری" - }, - "slack-now-playing": { - "name": "وضعیت Slack", - "description": "وضعیت Slack شما را به آهنگ در حال پخش تنظیم می‌کند", - "status-text": "در حال پخش: {{artist}} - {{title}}", - "menu": { - "settings": "تنظیمات", - "token": "توکن API Slack", - "cookie-token": "توکن کوکی Slack", - "emoji-name": "نام ایموجی سفارشی" - } } } } diff --git a/src/i18n/resources/fi.json b/src/i18n/resources/fi.json index bfc4dbaf87..adbecff330 100644 --- a/src/i18n/resources/fi.json +++ b/src/i18n/resources/fi.json @@ -648,17 +648,6 @@ } } } - }, - "slack-now-playing": { - "name": "Slack-tila", - "description": "Asettaa Slack-tilasi nykyisin soivaan kappaleeseen", - "status-text": "Nyt soi: {{artist}} - {{title}}", - "menu": { - "settings": "Asetukset", - "token": "Slack API -tunnus", - "cookie-token": "Slack Cookie -tunnus", - "emoji-name": "Mukautetun emojin nimi" - } } } } diff --git a/src/i18n/resources/fil.json b/src/i18n/resources/fil.json index 4d76e9447c..6aa8f03818 100644 --- a/src/i18n/resources/fil.json +++ b/src/i18n/resources/fil.json @@ -792,17 +792,6 @@ "menu": { "visualizer-type": "Uri ng Visualizer" } - }, - "slack-now-playing": { - "name": "Slack Status", - "description": "Sets your Slack status to the currently playing song", - "status-text": "Now Playing: {{artist}} - {{title}}", - "menu": { - "settings": "Settings", - "token": "Slack API Token", - "cookie-token": "Slack Cookie Token", - "emoji-name": "Custom Emoji Name" - } } } } diff --git a/src/i18n/resources/fr.json b/src/i18n/resources/fr.json index 436ffd9ae4..f5ffc1a828 100644 --- a/src/i18n/resources/fr.json +++ b/src/i18n/resources/fr.json @@ -392,17 +392,6 @@ }, "name": "Désactiver la lecture automatique" }, - "slack-now-playing": { - "description": "Définit votre statut Slack sur la chanson en cours de lecture", - "menu": { - "settings": "Paramètres", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Nom de l'emoji personnalisé" - }, - "name": "Statut Slack", - "status-text": "En écoute : {{artist}} - {{title}}" - }, "discord": { "backend": { "already-connected": "Tentative de connexion avec une connexion active", diff --git a/src/i18n/resources/he.json b/src/i18n/resources/he.json index cfeb6acbd9..54093e7ada 100644 --- a/src/i18n/resources/he.json +++ b/src/i18n/resources/he.json @@ -218,17 +218,6 @@ }, "name": "חוסם פרסומות" }, - "slack-now-playing": { - "name": "סטטוס Slack", - "description": "מגדיר את סטטוס ה-Slack שלך לשיר המושמע כעת", - "status-text": "מנגן כעת: {{artist}} - {{title}}", - "menu": { - "settings": "הגדרות", - "token": "אסימון API של Slack", - "cookie-token": "אסימון עוגייה של Slack", - "emoji-name": "שם אימוג'י מותאם אישית" - } - }, "album-actions": { "description": "מוסיף לחצני ביטול אהבתי, דיסלייק, 'אהבתי' ו'לא אהבתי' כדי להחיל זאת על כל השירים ברשימת השמעה או אלבום", "name": "פעולות אלבום" diff --git a/src/i18n/resources/hi.json b/src/i18n/resources/hi.json index d2f0066119..f2e1ed1f04 100644 --- a/src/i18n/resources/hi.json +++ b/src/i18n/resources/hi.json @@ -350,17 +350,6 @@ "label": "तरीका" } } - }, - "slack-now-playing": { - "name": "Slack स्थिति", - "description": "आपकी Slack स्थिति को वर्तमान में चल रहे गाने पर सेट करता है", - "status-text": "अभी बज रहा है: {{artist}} - {{title}}", - "menu": { - "settings": "सेटिंग्स", - "token": "Slack API टोकन", - "cookie-token": "Slack कुकी टोकन", - "emoji-name": "कस्टम इमोजी नाम" - } } } } diff --git a/src/i18n/resources/hr.json b/src/i18n/resources/hr.json index 2f4d98170e..b95c9ecf97 100644 --- a/src/i18n/resources/hr.json +++ b/src/i18n/resources/hr.json @@ -207,17 +207,6 @@ } }, "plugins": { - "slack-now-playing": { - "name": "Slack status", - "description": "Postavlja vaš Slack status na pjesmu koja se trenutno reproducira", - "status-text": "Sada svira: {{artist}} - {{title}}", - "menu": { - "settings": "Postavke", - "token": "Slack API token", - "cookie-token": "Slack Cookie token", - "emoji-name": "Naziv prilagođenog emojija" - } - }, "ad-speedup": { "description": "Ako se pokrene oglas, zvuk se isključi i brzina reprodukcije se postavi na 16x", "name": "Ubrzanje Oglasa" diff --git a/src/i18n/resources/hu.json b/src/i18n/resources/hu.json index 7a119b3aa3..4450b43864 100644 --- a/src/i18n/resources/hu.json +++ b/src/i18n/resources/hu.json @@ -840,17 +840,6 @@ "visualizer-type": "Vizualizáció típus" }, "name": "Vizualizáció" - }, - "slack-now-playing": { - "name": "Slack állapot", - "description": "Beállítja a Slack állapotod az éppen játszó dalra", - "status-text": "Most játszik: {{artist}} - {{title}}", - "menu": { - "settings": "Beállítások", - "token": "Slack API token", - "cookie-token": "Slack Cookie token", - "emoji-name": "Egyéni emoji név" - } } } } diff --git a/src/i18n/resources/id.json b/src/i18n/resources/id.json index af4e6b14a0..ac6548fe2b 100644 --- a/src/i18n/resources/id.json +++ b/src/i18n/resources/id.json @@ -840,17 +840,6 @@ "visualizer-type": "Tipe Visualisator" }, "name": "Visualisator" - }, - "slack-now-playing": { - "name": "Status Slack", - "description": "Mengatur status Slack Anda ke lagu yang sedang diputar", - "status-text": "Sedang Diputar: {{artist}} - {{title}}", - "menu": { - "settings": "Pengaturan", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Nama Emoji Kustom" - } } } } diff --git a/src/i18n/resources/is.json b/src/i18n/resources/is.json index 9b42846eb8..f58bc147b2 100644 --- a/src/i18n/resources/is.json +++ b/src/i18n/resources/is.json @@ -808,17 +808,6 @@ "visualizer-type": "Sýndarstýringartegund" }, "name": "Sýndarstýringar" - }, - "slack-now-playing": { - "name": "Slack staða", - "description": "Stillir Slack stöðuna þína á lagið sem er í spilun núna", - "status-text": "Spilar núna: {{artist}} - {{title}}", - "menu": { - "settings": "Stillingar", - "token": "Slack API lykill", - "cookie-token": "Slack köku lykill", - "emoji-name": "Sérsniðið táknmynd nafn" - } } } } diff --git a/src/i18n/resources/it.json b/src/i18n/resources/it.json index a6921e8c9e..c70a9d47d5 100644 --- a/src/i18n/resources/it.json +++ b/src/i18n/resources/it.json @@ -844,17 +844,6 @@ "visualizer-type": "Tipo di visualizzazione" }, "name": "Visualizzatore grafico" - }, - "slack-now-playing": { - "name": "Stato Slack", - "description": "Imposta il tuo stato Slack sulla canzone attualmente in riproduzione", - "status-text": "In riproduzione: {{artist}} - {{title}}", - "menu": { - "settings": "Impostazioni", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Nome emoji personalizzato" - } } } } diff --git a/src/i18n/resources/ja.json b/src/i18n/resources/ja.json index fa9ecc86c9..9a5fabb2e0 100644 --- a/src/i18n/resources/ja.json +++ b/src/i18n/resources/ja.json @@ -333,6 +333,20 @@ "description": "オーディオにコンプレッサーを適用します(信号での一番大きい部分の音量を下げ、小さい部分の音量を上げる)", "name": "オーディオコンプレッサー" }, + "auth-proxy-adapter": { + "menu": { + "disable": "プロキシアダプターを無効にする", + "enable": "プロキシアダプターを有効にする", + "hostname": { + "label": "ホスト名" + } + }, + "prompt": { + "hostname": { + "label": "ローカルプロキシサーバのホスト名を入力します(再起動が必要です):" + } + } + }, "blur-nav-bar": { "description": "ナビゲーションバーを透明かつぼやけにします", "name": "ナビゲーションバーの曇り効果" @@ -392,17 +406,6 @@ }, "name": "自動再生を無効化" }, - "slack-now-playing": { - "description": "現在再生中の曲をSlackステータスに設定します", - "menu": { - "settings": "設定", - "token": "Slack APIトークン", - "cookie-token": "Slack Cookieトークン", - "emoji-name": "カスタム絵文字名" - }, - "name": "Slackステータス", - "status-text": "再生中: {{artist}} - {{title}}" - }, "discord": { "backend": { "already-connected": "すでに有効になっている接続に接続を試みました", diff --git a/src/i18n/resources/ka.json b/src/i18n/resources/ka.json index c1120642ef..d1ac725221 100644 --- a/src/i18n/resources/ka.json +++ b/src/i18n/resources/ka.json @@ -303,17 +303,6 @@ } } } - }, - "slack-now-playing": { - "name": "Slack Status", - "description": "Sets your Slack status to the currently playing song", - "status-text": "Now Playing: {{artist}} - {{title}}", - "menu": { - "settings": "Settings", - "token": "Slack API Token", - "cookie-token": "Slack Cookie Token", - "emoji-name": "Custom Emoji Name" - } } } } diff --git a/src/i18n/resources/kn.json b/src/i18n/resources/kn.json index 761a933ae6..3595158f1a 100644 --- a/src/i18n/resources/kn.json +++ b/src/i18n/resources/kn.json @@ -3,19 +3,5 @@ "code": "kn", "local-name": "ಕನ್ನಡ", "name": "Kannada" - }, - "plugins": { - "slack-now-playing": { - "name": "Slack ಸ್ಥಿತಿ", - "description": "ನಿಮ್ಮ Slack ಸ್ಥಿತಿಯನ್ನು ಪ್ರಸ್ತುತ ಪ್ಲೇ ಆಗುತ್ತಿರುವ ಹಾಡಿಗೆ ಹೊಂದಿಸುತ್ತದೆ", - "status-text": "ಈಗ ಪ್ಲೇ ಆಗುತ್ತಿದೆ: {{artist}} - {{title}}", - "menu": { - "settings": "ಸೆಟ್ಟಿಂಗ್ಸ್", - "token": "Slack API ಟೋಕನ್", - "cookie-token": "Slack ಕುಕೀ ಟೋಕನ್", - "emoji-name": "ಕಸ್ಟಮ್ ಎಮೋಜಿ ಹೆಸರು" - } - } } } - diff --git a/src/i18n/resources/ko.json b/src/i18n/resources/ko.json index 7790f15ae4..fb32ecb5b1 100644 --- a/src/i18n/resources/ko.json +++ b/src/i18n/resources/ko.json @@ -844,17 +844,6 @@ "visualizer-type": "비주얼라이저 타입" }, "name": "비주얼라이저" - }, - "slack-now-playing": { - "name": "Slack 상태", - "description": "현재 재생 중인 노래로 Slack 상태를 설정합니다", - "status-text": "지금 재생 중: {{artist}} - {{title}}", - "menu": { - "settings": "설정", - "token": "Slack API 토큰", - "cookie-token": "Slack 쿠키 토큰", - "emoji-name": "사용자 정의 이모티콘 이름" - } } } } diff --git a/src/i18n/resources/lt.json b/src/i18n/resources/lt.json index 3cb2d599ad..b1c65c98b4 100644 --- a/src/i18n/resources/lt.json +++ b/src/i18n/resources/lt.json @@ -627,17 +627,6 @@ "visualizer-type": "Vizualizatoriaus tipas" }, "name": "Vizualizatorius" - }, - "slack-now-playing": { - "name": "Slack būsena", - "description": "Nustato jūsų Slack būseną pagal dabar grojamą dainą", - "status-text": "Dabar groja: {{artist}} - {{title}}", - "menu": { - "settings": "Nustatymai", - "token": "Slack API raktas", - "cookie-token": "Slack slapuko raktas", - "emoji-name": "Pasirinktinio emoji pavadinimas" - } } } } diff --git a/src/i18n/resources/ml.json b/src/i18n/resources/ml.json index 6b173f8fb9..e64f147d2d 100644 --- a/src/i18n/resources/ml.json +++ b/src/i18n/resources/ml.json @@ -53,18 +53,5 @@ "message": "\"{{pluginName}}\" restart ആവശ്യപെടുന്നു" } } - }, - "plugins": { - "slack-now-playing": { - "name": "Slack സ്റ്റാറ്റസ്", - "description": "നിങ്ങളുടെ Slack സ്റ്റാറ്റസ് നിലവിൽ പ്ലേ ചെയ്യുന്ന ഗാനത്തിലേക്ക് സെറ്റ് ചെയ്യുന്നു", - "status-text": "ഇപ്പോൾ പ്ലേ ചെയ്യുന്നത്: {{artist}} - {{title}}", - "menu": { - "settings": "ക്രമീകരണങ്ങൾ", - "token": "Slack API ടോക്കൺ", - "cookie-token": "Slack കുക്കി ടോക്കൺ", - "emoji-name": "കസ്റ്റം ഇമോജി പേര്" - } - } } } diff --git a/src/i18n/resources/ms.json b/src/i18n/resources/ms.json index d9c2526035..9eebf16657 100644 --- a/src/i18n/resources/ms.json +++ b/src/i18n/resources/ms.json @@ -6,6 +6,7 @@ "executed-at-ms": "Plugin {{pluginName}}::{{contextName}} dilaksanakan pada {{ms}}ms", "initialize-failed": "Gagal untuk memulakan plugin \"{{pluginName}}\"", "load-all": "Memuatkan semua plugin", + "load-failed": "Gagal untuk memuatkan \"{{pluginName}}\"", "loaded": "Plugin \"{{pluginName}}\" dimuatkan", "unload-failed": "Gagal untuk memunggah plugin \"{{pluginName}}\"", "unloaded": "Plugin \"{{pluginName}}\" dipunggahkan" @@ -31,19 +32,28 @@ "theme": { "css-file-not-found": "Fail CSS \"{{cssFile}}\" tidak wujud, mengabaikan" }, + "unresponsive": { + "details": "Ralat Tidak Bertindak Balas!\n{{error}}" + }, "when-ready": { "clearing-cache-after-20s": "Membersihkan cache aplikasi" + }, + "window": { + "tried-to-render-offscreen": "Tetingkap cuba dirender di luar skrin, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" } }, "dialog": { "hide-menu-enabled": { - "detail": "Menu telah disembunyikan, guna 'Alt' untuk menunjukkannya (atau 'Escape' jika menggunakan In-App Menu)" + "detail": "Menu telah disembunyikan, guna 'Alt' untuk menunjukkannya (atau 'Escape' jika menggunakan In-App Menu)", + "message": "Sembunyikan Menu telah diaktifkan", + "title": "Sembunyikan Menu diaktifkan" }, "need-to-restart": { "buttons": { "later": "Nanti", "restart-now": "Restart Sekarang" }, + "detail": "Plugin {{pluginName}} perlu dimulakan semula untuk berkuatkuasa", "message": "\"{{pluginName}}\" perlu dimulakan semula", "title": "Mulakan Semula Diperlukan" }, @@ -59,10 +69,12 @@ }, "update-available": { "buttons": { + "disable": "Lumpuhkan Kemas Kini", "download": "Muat Turun", "ok": "OK" }, - "detail": "Versi baharu kini tersedia dan boleh dimuat turun di {{downloadLink}}" + "detail": "Versi baharu kini tersedia dan boleh dimuat turun di {{downloadLink}}", + "message": "Versi baharu tersedia" } }, "menu": { @@ -198,17 +210,6 @@ "templates": { "button": "Lagu" } - }, - "slack-now-playing": { - "name": "Status Slack", - "description": "Menetapkan status Slack anda kepada lagu yang sedang dimainkan", - "status-text": "Sedang Dimainkan: {{artist}} - {{title}}", - "menu": { - "settings": "Tetapan", - "token": "Token API Slack", - "cookie-token": "Token Kuki Slack", - "emoji-name": "Nama Emoji Tersuai" - } } } } diff --git a/src/i18n/resources/nb.json b/src/i18n/resources/nb.json index ca13b3b7b0..bae167834a 100644 --- a/src/i18n/resources/nb.json +++ b/src/i18n/resources/nb.json @@ -586,17 +586,6 @@ "visualizer-type": "Visualisatortype" }, "name": "Visualisator" - }, - "slack-now-playing": { - "name": "Slack-status", - "description": "Setter Slack-statusen din til sangen som spilles for øyeblikket", - "status-text": "Spiller nå: {{artist}} - {{title}}", - "menu": { - "settings": "Innstillinger", - "token": "Slack API-token", - "cookie-token": "Slack Cookie-token", - "emoji-name": "Egendefinert emoji-navn" - } } } } diff --git a/src/i18n/resources/ne.json b/src/i18n/resources/ne.json index eeca2a3942..92f6cf9f14 100644 --- a/src/i18n/resources/ne.json +++ b/src/i18n/resources/ne.json @@ -333,6 +333,30 @@ "description": "अडियोमा कम्प्रेसन लागू गर्नुहोस् (सङ्केतको सबैभन्दा चर्को भागहरूको भोल्युम कम गर्दछ र नरम भागहरूको भोल्युम बढाउँछ)", "name": "अडियो कम्प्रेसर" }, + "auth-proxy-adapter": { + "description": "प्रमाणीकरण प्रोक्सी सेवाहरूको प्रयोगको लागि समर्थन", + "menu": { + "disable": "प्रोक्सी एडाप्टर बन्द गर्नुहोस्", + "enable": "प्रोक्सी एडाप्टर खोल्नुहोस्", + "hostname": { + "label": "होस्टनाम" + }, + "port": { + "label": "पोर्ट" + } + }, + "name": "प्रमाणीकरण प्रोक्सी एडाप्टर", + "prompt": { + "hostname": { + "label": "स्थानीय प्रोक्सी सर्भरको लागि होस्टनाम प्रविष्ट गर्नुहोस् (पुनःसुरु गर्न आवश्यक छ):", + "title": "प्रोक्सी होस्टनाम" + }, + "port": { + "label": "स्थानीय प्रोक्सी सर्भरको लागि पोर्ट प्रविष्ट गर्नुहोस् (पुनःसुरु गर्न आवश्यक छ):", + "title": "प्रोक्सी पोर्ट" + } + } + }, "blur-nav-bar": { "description": "नेभिगेसन बारलाई पारदर्शी र धुवाँलो बनाउँछ", "name": "ब्लर नेभिगेसन बार" @@ -600,6 +624,10 @@ }, "name": "सूचनाहरू" }, + "performance-improvement": { + "description": "प्रयोगात्मक स्क्रिप्टहरू सक्रिय गरेर कार्यसम्पादन सुधार गर्नुहोस्", + "name": "कार्यसम्पादन सुधार [प्रयोगात्मक]" + }, "picture-in-picture": { "description": "एपलाई पिक्चर-इन-पिक्चर मोडमा परिवर्तन गर्न अनुमति दिन्छ", "menu": { @@ -683,6 +711,7 @@ "listenbrainz": { "token": "ListenBrainz प्रयोगकर्ता टोकन प्रविष्ट गर्नुहोस्" }, + "scrobble-alternative-title": "वैकल्पिक शीर्षकहरू प्रयोग गर्नुहोस्", "scrobble-other-media": "अन्य मिडियालाई स्क्रबल गर्नुहोस्" }, "name": "स्क्रबबलर", @@ -767,6 +796,10 @@ "label": "गीतशब्दहरू पूर्ण रूपमा समक्रमित बनाउनुहोस्", "tooltip": "अर्को लाइनको प्रदर्शनलाई मिलिसेकेन्डमा गणना गर्नुहोस् (यसले प्रदर्शनमा हल्का प्रभाव पार्न सक्छ)" }, + "romanization": { + "label": "रोमनकृत शब्दहरू", + "tooltip": "यदि गीतका शब्दहरू फरक भाषामा छन् भने, ल्याटिन संस्करण प्रदर्शन गर्ने प्रयास गर्नुहोस्।" + }, "show-lyrics-even-if-inexact": { "label": "गीतशब्दहरू अपर्याप्त भए पनि देखाउनुहोस्", "tooltip": "यदि गीत फेला परेन भने, प्लगिनले फरक खोजी सोधपुछसँग पुन: प्रयास गर्छ।\nदोस्रो प्रयासको परिणाम ठ्याक्कै मिल्न नपनि सक्छ।" @@ -799,6 +832,10 @@ "description": "OBS को टुना प्लगइनसँग एकीकरण", "name": "टुना OBS" }, + "unobtrusive-player": { + "description": "गीत बजाउँदा प्लेयरलाई पप अप हुनबाट रोक्छ", + "name": "अवरोधरहित संगीत प्लेयर" + }, "video-toggle": { "description": "भिडियो/गीत मोड बीच स्विच गर्न बटन थप्दछ। वैकल्पिक रूपमा सम्पूर्ण भिडियो ट्याब हटाउन पनि सक्छ", "menu": { @@ -831,17 +868,6 @@ "visualizer-type": "भिजुअलाइजरको प्रकार" }, "name": "भिजुअलाइजर" - }, - "slack-now-playing": { - "name": "Slack स्टेटस", - "description": "तपाईंको Slack स्टेटसलाई हाल बजिरहेको गीतमा सेट गर्छ", - "status-text": "अहिले बज्दै: {{artist}} - {{title}}", - "menu": { - "settings": "सेटिङ्गहरु", - "token": "Slack API टोकन", - "cookie-token": "Slack कुकी टोकन", - "emoji-name": "कस्टम इमोजी नाम" - } } } } diff --git a/src/i18n/resources/nl.json b/src/i18n/resources/nl.json index 937ad173d7..433c6aa460 100644 --- a/src/i18n/resources/nl.json +++ b/src/i18n/resources/nl.json @@ -844,17 +844,6 @@ "visualizer-type": "Visualisatietype" }, "name": "Visualisator" - }, - "slack-now-playing": { - "name": "Slack Status", - "description": "Stelt je Slack-status in op het nummer dat momenteel wordt afgespeeld", - "status-text": "Nu aan het spelen: {{artist}} - {{title}}", - "menu": { - "settings": "Instellingen", - "token": "Slack API-token", - "cookie-token": "Slack Cookie-token", - "emoji-name": "Aangepaste emoji-naam" - } } } } diff --git a/src/i18n/resources/pl.json b/src/i18n/resources/pl.json index f033b97775..b2e93df3d2 100644 --- a/src/i18n/resources/pl.json +++ b/src/i18n/resources/pl.json @@ -844,17 +844,6 @@ "visualizer-type": "Typ wizualizatora" }, "name": "Wizualizator" - }, - "slack-now-playing": { - "name": "Status Slack", - "description": "Ustawia twój status Slack na aktualnie odtwarzaną piosenkę", - "status-text": "Teraz odtwarzane: {{artist}} - {{title}}", - "menu": { - "settings": "Ustawienia", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Nazwa niestandardowego emoji" - } } } } diff --git a/src/i18n/resources/pt-BR.json b/src/i18n/resources/pt-BR.json index 2997dd7c95..f617f699cb 100644 --- a/src/i18n/resources/pt-BR.json +++ b/src/i18n/resources/pt-BR.json @@ -333,6 +333,30 @@ "description": "Aplicar compressão ao áudio (reduz o volume das partes mais altas e aumenta o volume das partes mais baixas)", "name": "Compressor de áudio" }, + "auth-proxy-adapter": { + "description": "Suporte para o uso de serviços de proxy de autenticação", + "menu": { + "disable": "Desativar adaptador proxy", + "enable": "Ativar adaptador proxy", + "hostname": { + "label": "Nome do host" + }, + "port": { + "label": "Porta" + } + }, + "name": "Adaptador de proxy de autenticação", + "prompt": { + "hostname": { + "label": "Entre o nome do host do servidor proxy local (necessário reiniciar):", + "title": "Nome do host do proxy" + }, + "port": { + "label": "Entre a porta do servidor proxy local (necessário reiniciar):", + "title": "Porta do proxy" + } + } + }, "blur-nav-bar": { "description": "Torna a barra de navegação transparente e desfocada", "name": "Desfocar barra de navegação" @@ -392,17 +416,6 @@ }, "name": "Desativar reprodução automática" }, - "slack-now-playing": { - "description": "Define seu status do Slack para a música que está tocando atualmente", - "menu": { - "settings": "Configurações", - "token": "Token de API do Slack", - "cookie-token": "Token de Cookie do Slack", - "emoji-name": "Nome do emoji personalizado" - }, - "name": "Status do Slack", - "status-text": "Tocando agora: {{artist}} - {{title}}" - }, "discord": { "backend": { "already-connected": "Tentativa de conectar-se com conexão ativa", diff --git a/src/i18n/resources/pt.json b/src/i18n/resources/pt.json index 36bd5788a3..d8886bab44 100644 --- a/src/i18n/resources/pt.json +++ b/src/i18n/resources/pt.json @@ -840,17 +840,6 @@ "visualizer-type": "Tipo de visualizador" }, "name": "Visualizador" - }, - "slack-now-playing": { - "name": "Estado do Slack", - "description": "Define o seu estado do Slack para a música que está a tocar atualmente", - "status-text": "A tocar: {{artist}} - {{title}}", - "menu": { - "settings": "Configurações", - "token": "Token de API do Slack", - "cookie-token": "Token de Cookie do Slack", - "emoji-name": "Nome do emoji personalizado" - } } } } diff --git a/src/i18n/resources/ro.json b/src/i18n/resources/ro.json index 1523c5cc01..c5348ab139 100644 --- a/src/i18n/resources/ro.json +++ b/src/i18n/resources/ro.json @@ -830,17 +830,6 @@ "visualizer-type": "Tip de vizualizator" }, "name": "Vizualizator" - }, - "slack-now-playing": { - "name": "Status Slack", - "description": "Setează statusul tău de Slack la melodia care se redă în prezent", - "status-text": "În redare acum: {{artist}} - {{title}}", - "menu": { - "settings": "Setări", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Nume emoji personalizat" - } } } } diff --git a/src/i18n/resources/ru.json b/src/i18n/resources/ru.json index f647992142..7b752b356e 100644 --- a/src/i18n/resources/ru.json +++ b/src/i18n/resources/ru.json @@ -333,6 +333,30 @@ "description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)", "name": "Нормализация аудио" }, + "auth-proxy-adapter": { + "description": "Поддержка использования сервисов аутентификационного прокси", + "menu": { + "disable": "Отключить адаптер прокси", + "enable": "Включить адаптер прокси", + "hostname": { + "label": "Имя хоста" + }, + "port": { + "label": "Порт" + } + }, + "name": "Адаптер аутентификационного прокси", + "prompt": { + "hostname": { + "label": "Введите имя хоста для локального прокси-сервера (требуется перезапуск):", + "title": "Имя хоста прокси" + }, + "port": { + "label": "Введите порт для локального прокси-сервера (требуется перезапуск):", + "title": "Порт прокси" + } + } + }, "blur-nav-bar": { "description": "Делает панель навигации прозрачной и размытой", "name": "Размытие панели навигации" @@ -844,17 +868,6 @@ "visualizer-type": "Вид визуализации" }, "name": "Визуализатор" - }, - "slack-now-playing": { - "name": "Статус Slack", - "description": "Устанавливает ваш статус Slack на текущую песню", - "status-text": "Сейчас играет: {{artist}} - {{title}}", - "menu": { - "settings": "Настройки", - "token": "Токен API Slack", - "cookie-token": "Токен Cookie Slack", - "emoji-name": "Имя пользовательского эмодзи" - } } } } diff --git a/src/i18n/resources/si.json b/src/i18n/resources/si.json index 8adf12bd0f..687b7676ed 100644 --- a/src/i18n/resources/si.json +++ b/src/i18n/resources/si.json @@ -104,18 +104,5 @@ } } } - }, - "plugins": { - "slack-now-playing": { - "name": "Slack තත්ත්වය", - "description": "ඔබේ Slack තත්ත්වය දැනට වාදනය වන ගීතයට සැකසේ", - "status-text": "දැන් වාදනය වන්නේ: {{artist}} - {{title}}", - "menu": { - "settings": "සැකසුම්", - "token": "Slack API ටෝකනය", - "cookie-token": "Slack කුකී ටෝකනය", - "emoji-name": "අභිමත ඉමෝජි නම" - } - } } } diff --git a/src/i18n/resources/sl.json b/src/i18n/resources/sl.json index 82628afe8f..57333dc17b 100644 --- a/src/i18n/resources/sl.json +++ b/src/i18n/resources/sl.json @@ -548,17 +548,6 @@ "id-copied": "Host ID je kopiran v odložišče", "id-copy-failed": "Host ID ni bilo mogoče kopirati" } - }, - "slack-now-playing": { - "name": "Slack status", - "description": "Nastavi vaš Slack status na trenutno predvajano pesem", - "status-text": "Zdaj predvaja: {{artist}} - {{title}}", - "menu": { - "settings": "Nastavitve", - "token": "Slack API žeton", - "cookie-token": "Slack Cookie žeton", - "emoji-name": "Ime emoji po meri" - } } } } diff --git a/src/i18n/resources/sr.json b/src/i18n/resources/sr.json index 8608457f7b..b65f5a4591 100644 --- a/src/i18n/resources/sr.json +++ b/src/i18n/resources/sr.json @@ -3,19 +3,5 @@ "code": "sr", "local-name": "Српски", "name": "Serbian" - }, - "plugins": { - "slack-now-playing": { - "name": "Slack статус", - "description": "Поставља ваш Slack статус на песму која се тренутно репродукује", - "status-text": "Сада свира: {{artist}} - {{title}}", - "menu": { - "settings": "Подешавања", - "token": "Slack API токен", - "cookie-token": "Slack Cookie токен", - "emoji-name": "Назив прилагођеног емоджија" - } - } } } - diff --git a/src/i18n/resources/sv.json b/src/i18n/resources/sv.json index 620146a8de..b213fbdb52 100644 --- a/src/i18n/resources/sv.json +++ b/src/i18n/resources/sv.json @@ -224,17 +224,6 @@ "templates": { "button": "Låt" } - }, - "slack-now-playing": { - "name": "Slack-status", - "description": "Ställer in din Slack-status till låten som spelas för närvarande", - "status-text": "Spelar nu: {{artist}} - {{title}}", - "menu": { - "settings": "Inställningar", - "token": "Slack API-token", - "cookie-token": "Slack Cookie-token", - "emoji-name": "Anpassat emoji-namn" - } } } } diff --git a/src/i18n/resources/ta.json b/src/i18n/resources/ta.json index 8f621e7edb..3c6bc6fd98 100644 --- a/src/i18n/resources/ta.json +++ b/src/i18n/resources/ta.json @@ -831,17 +831,6 @@ "visualizer-type": "விசுவலைசர் வகை" }, "name": "காட்சிப்படுத்தல்" - }, - "slack-now-playing": { - "name": "Slack நிலை", - "description": "உங்கள் Slack நிலையை தற்போது இயங்கும் பாடலுக்கு அமைக்கிறது", - "status-text": "இப்போது இயங்குகிறது: {{artist}} - {{title}}", - "menu": { - "settings": "அமைப்புகள்", - "token": "Slack API டோக்கன்", - "cookie-token": "Slack குக்கீ டோக்கன்", - "emoji-name": "தனிப்பயன் எமோஜி பெயர்" - } } } } diff --git a/src/i18n/resources/th.json b/src/i18n/resources/th.json index 009bf782f8..b72f43d283 100644 --- a/src/i18n/resources/th.json +++ b/src/i18n/resources/th.json @@ -840,17 +840,6 @@ "visualizer-type": "ประเภทวิชวลไลเซอร์" }, "name": "วิชวลไลเซอร์" - }, - "slack-now-playing": { - "name": "สถานะ Slack", - "description": "ตั้งค่าสถานะ Slack ของคุณเป็นเพลงที่กำลังเล่นอยู่", - "status-text": "กำลังเล่น: {{artist}} - {{title}}", - "menu": { - "settings": "การตั้งค่า", - "token": "โทเค็น API Slack", - "cookie-token": "โทเค็นคุกกี้ Slack", - "emoji-name": "ชื่ออีโมจิที่กำหนดเอง" - } } } } diff --git a/src/i18n/resources/tr.json b/src/i18n/resources/tr.json index 1d7dedd096..9ecbb09dcd 100644 --- a/src/i18n/resources/tr.json +++ b/src/i18n/resources/tr.json @@ -333,6 +333,30 @@ "description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)", "name": "Ses Sıkıştırma" }, + "auth-proxy-adapter": { + "description": "Kimlik doğrulama proxy hizmetlerinin kullanımına destek", + "menu": { + "disable": "Proxy Bağdaştırıcısını Devre Dışı Bırak", + "enable": "Proxy Bağdaştırıcısını Etkinleştir", + "hostname": { + "label": "Ana makine adı" + }, + "port": { + "label": "Port" + } + }, + "name": "Ara Sunucu Doğrulama Bağdaştırıcısı", + "prompt": { + "hostname": { + "label": "Yerel proxy sunucusu için ana makine adını girin (yeniden başlatma gerektirir):", + "title": "Proxy Ana Makine Adı" + }, + "port": { + "label": "Yerel proxy sunucusu için bağlantı noktasını girin (yeniden başlatma gerektirir):", + "title": "Proxy Bağlantı Noktası" + } + } + }, "blur-nav-bar": { "description": "Gezinme çubuğunu şeffaf ve bulanık yapar", "name": "Navigasyon barını bulanıklaştır" @@ -600,6 +624,10 @@ }, "name": "Bildirimler" }, + "performance-improvement": { + "description": "Deneysel komut dosyalarını etkinleştirerek performansı artırın", + "name": "Performans iyileştirmesi [Beta]" + }, "picture-in-picture": { "description": "Uygulamayı resim-içinde-resim moduna geçirmeye izin verir", "menu": { @@ -840,17 +868,6 @@ "visualizer-type": "Görselleştirici Tipi" }, "name": "Görselleştirici" - }, - "slack-now-playing": { - "name": "Slack Durumu", - "description": "Slack durumunuzu şu anda çalan şarkıya ayarlar", - "status-text": "Şu anda çalıyor: {{artist}} - {{title}}", - "menu": { - "settings": "Ayarlar", - "token": "Slack API Jetonu", - "cookie-token": "Slack Çerez Jetonu", - "emoji-name": "Özel Emoji Adı" - } } } } diff --git a/src/i18n/resources/uk.json b/src/i18n/resources/uk.json index c14a8cd7b5..940c76481a 100644 --- a/src/i18n/resources/uk.json +++ b/src/i18n/resources/uk.json @@ -844,17 +844,6 @@ "visualizer-type": "Тип візуалізації" }, "name": "Візуалізація" - }, - "slack-now-playing": { - "name": "Статус Slack", - "description": "Встановлює ваш статус Slack на пісню, яка зараз грає", - "status-text": "Зараз грає: {{artist}} - {{title}}", - "menu": { - "settings": "Налаштування", - "token": "Токен API Slack", - "cookie-token": "Токен Cookie Slack", - "emoji-name": "Назва користувацького емодзі" - } } } } diff --git a/src/i18n/resources/ur.json b/src/i18n/resources/ur.json index dfd49d10a0..2602c00a68 100644 --- a/src/i18n/resources/ur.json +++ b/src/i18n/resources/ur.json @@ -200,18 +200,5 @@ "quit": "باہر نکلیں", "restart": "ایپ دوبارہ شروع کریں" } - }, - "plugins": { - "slack-now-playing": { - "name": "Slack حیثیت", - "description": "آپ کی Slack حیثیت کو موجودہ چل رہے گانے پر سیٹ کرتا ہے", - "status-text": "اب چل رہا ہے: {{artist}} - {{title}}", - "menu": { - "settings": "ترتیبات", - "token": "Slack API ٹوکن", - "cookie-token": "Slack کوکی ٹوکن", - "emoji-name": "حسب ضرورت ایموجی نام" - } - } } } diff --git a/src/i18n/resources/vi.json b/src/i18n/resources/vi.json index aac5a83c51..4b5996e46a 100644 --- a/src/i18n/resources/vi.json +++ b/src/i18n/resources/vi.json @@ -824,17 +824,6 @@ "visualizer-type": "Loại trình hiển thị" }, "name": "Trình hiển thị" - }, - "slack-now-playing": { - "name": "Trạng thái Slack", - "description": "Đặt trạng thái Slack của bạn thành bài hát đang phát hiện tại", - "status-text": "Đang phát: {{artist}} - {{title}}", - "menu": { - "settings": "Cài đặt", - "token": "Token API Slack", - "cookie-token": "Token Cookie Slack", - "emoji-name": "Tên emoji tùy chỉnh" - } } } } diff --git a/src/i18n/resources/zh-CN.json b/src/i18n/resources/zh-CN.json index a671f55c86..d54ef98dac 100644 --- a/src/i18n/resources/zh-CN.json +++ b/src/i18n/resources/zh-CN.json @@ -333,6 +333,30 @@ "description": "对音频应用压缩(压低响亮部分,提升柔和部分)", "name": "音频压缩器" }, + "auth-proxy-adapter": { + "name": "认证代理适配", + "description": "支持使用需要身份验证的代理", + "menu": { + "disable": "禁用代理适配", + "enable": "启用代理适配", + "hostname": { + "label": "主机名" + }, + "port": { + "label": "端口" + } + }, + "prompt": { + "hostname": { + "title": "代理主机名", + "label": "请输入本地代理服务器的主机名(需要重启):" + }, + "port": { + "title": "代理端口", + "label": "请输入本地代理服务器的端口号(需要重启):" + } + } + }, "blur-nav-bar": { "description": "让导航栏透明及模糊", "name": "模糊导航栏" @@ -844,17 +868,6 @@ "visualizer-type": "可视化类型" }, "name": "可视化效果" - }, - "slack-now-playing": { - "name": "Slack 状态", - "description": "将您的 Slack 状态设置为当前正在播放的歌曲", - "status-text": "正在播放: {{artist}} - {{title}}", - "menu": { - "settings": "设置", - "token": "Slack API 令牌", - "cookie-token": "Slack Cookie 令牌", - "emoji-name": "自定义表情名称" - } } } } diff --git a/src/i18n/resources/zh-TW.json b/src/i18n/resources/zh-TW.json index 38277a5acb..c59d3f6ae6 100644 --- a/src/i18n/resources/zh-TW.json +++ b/src/i18n/resources/zh-TW.json @@ -333,6 +333,30 @@ "description": "使用音效壓縮 (大聲部份的音量降低,柔和部份的音量提高)", "name": "音效壓縮器" }, + "auth-proxy-adapter": { + "description": "支援使用 Proxy 驗證服務", + "menu": { + "disable": "中斷 Proxy 連線", + "enable": "啟用 Proxy 連線", + "hostname": { + "label": "主機名稱" + }, + "port": { + "label": "連接埠" + } + }, + "name": "Proxy 連線驗證", + "prompt": { + "hostname": { + "label": "本地 Proxy 伺服器主機名稱(需要重啟應用):", + "title": "Proxy 主機名稱" + }, + "port": { + "label": "本地 Proxy 伺服器連接埠(需要重啟應用):", + "title": "Proxy 連接埠" + } + } + }, "blur-nav-bar": { "description": "使導覽列透明及模糊", "name": "模糊導覽列" @@ -844,17 +868,6 @@ "visualizer-type": "視覺化效果類型" }, "name": "視覺化效果" - }, - "slack-now-playing": { - "name": "Slack 狀態", - "description": "將您的 Slack 狀態設定為目前正在播放的歌曲", - "status-text": "正在播放: {{artist}} - {{title}}", - "menu": { - "settings": "設定", - "token": "Slack API 令牌", - "cookie-token": "Slack Cookie 令牌", - "emoji-name": "自定義表情符號名稱" - } } } } From 4eb1211c69949f536ab71654052f792660388a68 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Sun, 18 May 2025 16:12:46 -0600 Subject: [PATCH 27/29] refactor: remove unnecessary fetch options comment in slack API client --- src/plugins/slack-now-playing/slack-api-client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index 35540e1fb3..82398571bf 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -218,8 +218,7 @@ export class SlackApiClient { */ private createFetchOptions(headers: Record, options: { disableSSLValidation?: boolean } = {}): RequestInit { const fetchOptions: RequestInit = { - headers, - // 'credentials' and 'mode' are not needed for Node.js fetch + headers }; // For SSL validation disabling, use agent in Node.js if (options.disableSSLValidation) { From 9d9e9878062157d2c2f94565620caabe0904a9ec Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 2 Jan 2026 20:58:10 -0700 Subject: [PATCH 28/29] fix(slack-now-playing): fix API authentication after upstream sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Send token in request body instead of Authorization header - URL-encode cookie value for proper handling of + and / chars - Use native Node.js fetch (Chromium's net.fetch forbids Cookie headers) - Remove unused formdata-node dependency, use native FormData - Fix TypeScript strict mode issues (any → unknown, proper type guards) - Sync with upstream pear-devs/pear-desktop 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package.json | 8 +- src/plugins/slack-now-playing/index.ts | 2 +- src/plugins/slack-now-playing/main.ts | 349 ++++++++++++------ src/plugins/slack-now-playing/menu.ts | 19 +- .../slack-now-playing/slack-api-client.ts | 137 +++---- 5 files changed, 323 insertions(+), 192 deletions(-) diff --git a/package.json b/package.json index 5371649300..f105a9b165 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "\u0079\u006f\u0075\u0074\u0075\u0062\u0065\u002d\u006d\u0075\u0073\u0069\u0063", - "desktopName": "com.github.th_ch.\u0079\u006f\u0075\u0074\u0075\u0062\u0065\u005f\u006d\u0075\u0073\u0069\u0063", - "productName": "\u0059\u006f\u0075\u0054\u0075\u0062\u0065\u0020\u004d\u0075\u0073\u0069\u0063", + "name": "youtube-music", + "desktopName": "com.github.th_ch.youtube_music", + "productName": "YouTube Music", "version": "3.11.0", - "description": "\u0059\u006f\u0075\u0054\u0075\u0062\u0065\u0020\u004d\u0075\u0073\u0069\u0063 Desktop App - including custom plugins", + "description": "YouTube Music Desktop App - including custom plugins", "main": "./dist/main/index.js", "type": "module", "license": "MIT", diff --git a/src/plugins/slack-now-playing/index.ts b/src/plugins/slack-now-playing/index.ts index bfeb4a4047..c918ba8391 100644 --- a/src/plugins/slack-now-playing/index.ts +++ b/src/plugins/slack-now-playing/index.ts @@ -1,6 +1,6 @@ import { createPlugin } from '@/utils'; import { onMenu } from './menu'; -import { backend, SlackNowPlayingConfig } from './main'; +import { backend, type SlackNowPlayingConfig } from './main'; import { t } from '@/i18n'; export default createPlugin({ diff --git a/src/plugins/slack-now-playing/main.ts b/src/plugins/slack-now-playing/main.ts index a498cad813..20f59768c5 100644 --- a/src/plugins/slack-now-playing/main.ts +++ b/src/plugins/slack-now-playing/main.ts @@ -1,12 +1,16 @@ -import { net } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; + +import { net } from 'electron'; + import { SlackApiClient, SlackError } from './slack-api-client'; import { createBackend } from '@/utils'; -import registerCallback, { SongInfoEvent } from '@/providers/song-info'; +import { registerCallback, SongInfoEvent } from '@/providers/song-info'; import { t } from '@/i18n'; + import type { SongInfo } from '@/providers/song-info'; +import type { BackendContext } from '@/types/contexts'; // Plugin config type export interface SlackNowPlayingConfig { @@ -22,17 +26,27 @@ export interface SlackNowPlayingConfig { * @param config The object to check * @returns True if the object is a valid SlackNowPlayingConfig */ -function isSlackNowPlayingConfig(config: unknown): config is SlackNowPlayingConfig { +function isSlackNowPlayingConfig( + config: unknown, +): config is SlackNowPlayingConfig { if (!config || typeof config !== 'object') return false; const c = config as Partial; - return typeof c.enabled === 'boolean' && - typeof c.token === 'string' && - typeof c.cookieToken === 'string' && - typeof c.emojiName === 'string'; + return ( + typeof c.enabled === 'boolean' && + typeof c.token === 'string' && + typeof c.cookieToken === 'string' && + typeof c.emojiName === 'string' + ); } -const defaultEmojis = [':cd:', ':headphones:', ':musical_note:', ':notes:', ':radio:']; +const defaultEmojis = [ + ':cd:', + ':headphones:', + ':musical_note:', + ':notes:', + ':radio:', +]; // Cache to store album art file paths by URL to avoid repeated downloads type AlbumArtCache = { @@ -50,7 +64,7 @@ const state = { albumArtCache: {} as AlbumArtCache, // Cache album art files cacheExpiryMs: 30 * 60 * 1000, // Cache expiry time (30 minutes) cacheCleanupTimer: undefined as NodeJS.Timeout | undefined, // Timer for periodic cache cleanup - context: undefined as any, // Store the plugin context + context: undefined as BackendContext | undefined, // Store the plugin context currentConfig: undefined as SlackNowPlayingConfig | undefined, // Current configuration }; @@ -71,7 +85,8 @@ async function cleanupTempFiles(): Promise { for (const filePath of state.tempFiles) { try { // Check if the file exists before attempting to delete it - await fsPromises.access(filePath, fs.constants.F_OK) + await fsPromises + .access(filePath, fs.constants.F_OK) .then(() => fsPromises.unlink(filePath)) .then(() => { // Remove the file from the set once it's deleted @@ -83,7 +98,7 @@ async function cleanupTempFiles(): Promise { console.error(`Error deleting temporary file ${filePath}:`, error); } }); - } catch (error: any) { + } catch (error: unknown) { // Catch any unexpected errors if (error instanceof Error) { console.error(`Error during cleanup of ${filePath}:`, error.message); @@ -113,7 +128,7 @@ async function cleanupExpiredCache(): Promise { await fsPromises.access(cacheEntry.filePath, fs.constants.F_OK); await fsPromises.unlink(cacheEntry.filePath); state.tempFiles.delete(cacheEntry.filePath); - } catch (error: any) { + } catch { // Ignore errors if the file doesn't exist or is in use } } @@ -147,19 +162,23 @@ function validateConfig(config: SlackNowPlayingConfig): ValidationResult { if (!config.cookieToken) { errors.push('Missing Slack cookie token'); } else if (!config.cookieToken.startsWith('xoxd-')) { - errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); + errors.push( + 'Invalid Slack cookie token format (should start with "xoxd-")', + ); } // Check emoji name if (!config.emojiName) { errors.push('Missing custom emoji name'); } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { - errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); + errors.push( + 'Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)', + ); } return { valid: errors.length === 0, - errors + errors, }; } @@ -168,10 +187,14 @@ function validateConfig(config: SlackNowPlayingConfig): ValidationResult { * @param config The configuration to validate * @throws Error if the configuration is invalid */ -function assertValidConfig(config: SlackNowPlayingConfig): asserts config is SlackNowPlayingConfig { +function assertValidConfig( + config: SlackNowPlayingConfig, +): asserts config is SlackNowPlayingConfig { const result = validateConfig(config); if (!result.valid) { - throw new Error(`Invalid Slack Now Playing configuration: ${result.errors.join(', ')}`); + throw new Error( + `Invalid Slack Now Playing configuration: ${result.errors.join(', ')}`, + ); } } @@ -180,12 +203,17 @@ function assertValidConfig(config: SlackNowPlayingConfig): asserts config is Sla * @param songInfo Information about the current song * @param config Plugin configuration */ -async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) { +async function setNowPlaying( + songInfo: SongInfo, + config: SlackNowPlayingConfig, +) { try { // Validate configuration const validationResult = validateConfig(config); if (!validationResult.valid) { - console.error(`Cannot set Slack status: ${validationResult.errors.join(', ')}`); + console.error( + `Cannot set Slack status: ${validationResult.errors.join(', ')}`, + ); return; } @@ -196,7 +224,8 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) const title = songInfo.alternativeTitle ?? songInfo.title; const artistPart = songInfo.artist || 'Unknown Artist'; - const truncatedArtist = artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; + const truncatedArtist = + artistPart.length > 50 ? artistPart.substring(0, 50) + '...' : artistPart; // Use localized version of the status text let statusText = t('plugins.slack-now-playing.status-text') @@ -204,29 +233,40 @@ async function setNowPlaying(songInfo: SongInfo, config: SlackNowPlayingConfig) .replace('{{title}}', title); // Ensure the status text doesn't exceed Slack's limit - if (statusText.length > 97) statusText = statusText.substring(0, 97) + '...'; + if (statusText.length > 97) + statusText = statusText.substring(0, 97) + '...'; // Calculate expiration time (current time + remaining song duration) const elapsed = songInfo.elapsedSeconds ?? 0; const remaining = Math.max(0, Math.floor(songInfo.songDuration - elapsed)); const expirationTime = Math.floor(Date.now() / 1000) + remaining; - await updateSlackStatusWithEmoji(statusText, expirationTime, songInfo, config); - } catch (error: any) { + await updateSlackStatusWithEmoji( + statusText, + expirationTime, + songInfo, + config, + ); + } catch (error: unknown) { // Provide more detailed error information based on error type if (error instanceof Error) { console.error(`Error setting Slack status: ${error.message}`, { name: error.name, - stack: error.stack + stack: error.stack, }); } else { console.error(`Error setting Slack status: ${String(error)}`); } // Re-throw specific errors that should be handled by the caller - if (error instanceof Error && - (error.message.includes('token') || error.message.includes('authentication'))) { - throw new Error('Slack authentication failed. Please check your API token and cookie token.'); + if ( + error instanceof Error && + (error.message.includes('token') || + error.message.includes('authentication')) + ) { + throw new Error( + 'Slack authentication failed. Please check your API token and cookie token.', + ); } } } @@ -249,7 +289,9 @@ async function updateSlackStatusWithEmoji( // Validate configuration const validationResult = validateConfig(config); if (!validationResult.valid) { - throw new Error(`Cannot update Slack status: ${validationResult.errors.join(', ')}`); + throw new Error( + `Cannot update Slack status: ${validationResult.errors.join(', ')}`, + ); } const client = new SlackApiClient(config.token, config.cookieToken); @@ -273,7 +315,6 @@ async function updateSlackStatusWithEmoji( // Update state with the new status and emoji state.lastStatus = statusText; state.lastEmoji = statusEmoji; - } catch (error: unknown) { // Handle SlackError specifically if (error instanceof SlackError) { @@ -287,7 +328,7 @@ async function updateSlackStatusWithEmoji( else if (error instanceof Error) { console.error(`Error updating Slack status: ${error.message}`, { name: error.name, - stack: error.stack + stack: error.stack, }); } else { console.error(`Error updating Slack status: ${String(error)}`); @@ -298,8 +339,11 @@ async function updateSlackStatusWithEmoji( } } -async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { - if (songInfo.imageSrc && await uploadEmojiToSlack(songInfo, config)) { +async function getStatusEmoji( + songInfo: SongInfo, + config: SlackNowPlayingConfig, +): Promise { + if (songInfo.imageSrc && (await uploadEmojiToSlack(songInfo, config))) { return `:${config.emojiName}:`; } @@ -313,12 +357,17 @@ async function getStatusEmoji(songInfo: SongInfo, config: SlackNowPlayingConfig) * @param config Plugin configuration * @returns True if the emoji was successfully uploaded, false otherwise */ -async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingConfig): Promise { +async function uploadEmojiToSlack( + songInfo: SongInfo, + config: SlackNowPlayingConfig, +): Promise { try { // Validate configuration const validationResult = validateConfig(config); if (!validationResult.valid) { - console.error(`Cannot upload emoji to Slack: ${validationResult.errors.join(', ')}`); + console.error( + `Cannot upload emoji to Slack: ${validationResult.errors.join(', ')}`, + ); return false; } @@ -346,11 +395,13 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon // Read the file as a Buffer and append directly to FormData const fileBuffer = await fs.promises.readFile(filePath); const filename = path.basename(filePath) || 'emoji.png'; - const imageFile = new File([fileBuffer], filename); + // Convert Buffer to Uint8Array for File constructor compatibility + const imageFile = new File([new Uint8Array(fileBuffer)], filename); formData.append('image', imageFile, filename); - - } catch (fileError: any) { - console.error(`Error preparing album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); + } catch (fileError: unknown) { + console.error( + `Error preparing album art file: ${fileError instanceof Error ? fileError.message : String(fileError)}`, + ); return false; } @@ -364,31 +415,46 @@ async function uploadEmojiToSlack(songInfo: SongInfo, config: SlackNowPlayingCon const errorCode = apiError.responseData.error; if (errorCode === 'invalid_name') { - console.error(`Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`); + console.error( + `Invalid emoji name: ${config.emojiName}. Emoji names can only contain lowercase letters, numbers, hyphens, and underscores.`, + ); } else if (errorCode === 'too_large') { - console.error('Album art image is too large for Slack emoji (max 128KB).'); + console.error( + 'Album art image is too large for Slack emoji (max 128KB).', + ); } else if (errorCode === 'name_taken') { - console.error(`Emoji name '${config.emojiName}' is already taken. This should not happen as we check for existing emojis.`); + console.error( + `Emoji name '${config.emojiName}' is already taken. This should not happen as we check for existing emojis.`, + ); } else { - console.error(`Error uploading emoji: ${errorCode}`, apiError.responseData); + console.error( + `Error uploading emoji: ${errorCode}`, + apiError.responseData, + ); } // Log the full Slack error response for diagnostics console.error('Slack error full response:', apiError.responseData); - } else { - console.error(`Error uploading emoji to Slack: ${apiError instanceof Error ? apiError.message : String(apiError)}`); + console.error( + `Error uploading emoji to Slack: ${apiError instanceof Error ? apiError.message : String(apiError)}`, + ); } return false; } } catch (error: unknown) { // Handle any other unexpected errors if (error instanceof Error) { - console.error(`Unexpected error uploading emoji to Slack: ${error.message}`, { - name: error.name, - stack: error.stack - }); + console.error( + `Unexpected error uploading emoji to Slack: ${error.message}`, + { + name: error.name, + stack: error.stack, + }, + ); } else { - console.error(`Unexpected error uploading emoji to Slack: ${String(error)}`); + console.error( + `Unexpected error uploading emoji to Slack: ${String(error)}`, + ); } return false; } @@ -418,7 +484,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { // Verify the file still exists await fs.promises.access(cachedImage.filePath, fs.constants.F_OK); return cachedImage.filePath; - } catch (error: any) { + } catch { // File doesn't exist anymore, remove from cache delete state.albumArtCache[imageUrl]; } @@ -429,7 +495,7 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { try { await fs.promises.unlink(cachedImage.filePath); state.tempFiles.delete(cachedImage.filePath); - } catch (error: any) { + } catch { // Ignore errors if the file doesn't exist } } @@ -448,11 +514,15 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { response = await net.fetch(imageUrl); if (!response.ok) { - console.error(`Failed to fetch album art: HTTP ${response.status} ${response.statusText}`); + console.error( + `Failed to fetch album art: HTTP ${response.status} ${response.statusText}`, + ); return null; } } catch (fetchError) { - console.error(`Network error fetching album art: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); + console.error( + `Network error fetching album art: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`, + ); return null; } @@ -466,7 +536,9 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { return null; } } catch (bufferError) { - console.error(`Error processing album art data: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`); + console.error( + `Error processing album art data: ${bufferError instanceof Error ? bufferError.message : String(bufferError)}`, + ); return null; } @@ -482,20 +554,22 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { // Add to cache state.albumArtCache[imageUrl] = { filePath, - timestamp: now + timestamp: now, }; return filePath; } catch (fileError) { - console.error(`Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`); + console.error( + `Error writing album art to file: ${fileError instanceof Error ? fileError.message : String(fileError)}`, + ); return null; } - } catch (error: any) { + } catch (error: unknown) { // Catch any other unexpected errors if (error instanceof Error) { console.error(`Error saving album art to file: ${error.message}`, { name: error.name, - stack: error.stack + stack: error.stack, }); } else { console.error(`Error saving album art to file: ${String(error)}`); @@ -509,12 +583,16 @@ async function saveAlbumArtToFile(songInfo: SongInfo): Promise { * @param config Plugin configuration * @returns True if the emoji doesn't exist or was successfully deleted, false otherwise */ -async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { +async function ensureEmojiDoesNotExist( + config: SlackNowPlayingConfig, +): Promise { try { // Validate configuration const validationResult = validateConfig(config); if (!validationResult.valid) { - console.error(`Cannot check emoji existence: ${validationResult.errors.join(', ')}`); + console.error( + `Cannot check emoji existence: ${validationResult.errors.join(', ')}`, + ); return false; } @@ -529,27 +607,39 @@ async function ensureEmojiDoesNotExist(config: SlackNowPlayingConfig): Promise { +async function deleteExistingEmoji( + config: SlackNowPlayingConfig, +): Promise { try { // Validate configuration const validationResult = validateConfig(config); if (!validationResult.valid) { - console.error(`Cannot delete emoji: ${validationResult.errors.join(', ')}`); + console.error( + `Cannot delete emoji: ${validationResult.errors.join(', ')}`, + ); return false; } @@ -594,7 +693,7 @@ async function deleteExistingEmoji(config: SlackNowPlayingConfig): Promise { + ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => { process.on(signal, async () => { await cleanupTempFiles(); process.exit(0); @@ -660,7 +767,7 @@ export const backend = createBackend({ */ async start(ctx) { // Store the context and window for later use - state.context = ctx; + state.context = ctx as BackendContext; state.window = ctx.window; // Register exit handlers for cleanup @@ -671,7 +778,7 @@ export const backend = createBackend({ const cacheCleanupTimer = setInterval(async () => { try { await cleanupExpiredCache(); - } catch (error: any) { + } catch { // Ignore errors in the background task } }, cacheCleanupInterval); @@ -684,7 +791,8 @@ export const backend = createBackend({ state.currentConfig = initialConfig as SlackNowPlayingConfig; // Register callback to listen for song changes - registerCallback(async (songInfo, event) => { + + registerCallback(async (songInfo: SongInfo, event: SongInfoEvent) => { // Skip time change events if (event === SongInfoEvent.TimeChanged) return; @@ -714,38 +822,49 @@ export const backend = createBackend({ if (!validationResult.valid) { // Log a warning only on the first occurrence to avoid spamming the console if (!state.lastStatus) { - console.warn(`Slack Now Playing configuration validation failed: ${validationResult.errors.join(', ')}`); + console.warn( + `Slack Now Playing configuration validation failed: ${validationResult.errors.join(', ')}`, + ); } return; } // Process the song info with the latest config - await setNowPlaying(songInfo, config) - .catch(error => { - // Handle specific error types - if (error instanceof Error) { - // Check for authentication errors - if (error.message.includes('authentication') || error.message.includes('token')) { - console.error('Slack authentication failed. Please check your API token and cookie token.'); - } - // Check for rate limiting errors - else if (error.message.includes('rate limit') || error.message.includes('rate_limited')) { - console.error('Slack API rate limit exceeded. Please try again later.'); - } - // Generic error handling - else { - console.error(`Error in Slack Now Playing: ${error.message}`); - } - } else { - console.error(`Error in Slack Now Playing: ${String(error)}`); + await setNowPlaying(songInfo, config).catch((error) => { + // Handle specific error types + if (error instanceof Error) { + // Check for authentication errors + if ( + error.message.includes('authentication') || + error.message.includes('token') + ) { + console.error( + 'Slack authentication failed. Please check your API token and cookie token.', + ); } - }); - } catch (error: any) { + // Check for rate limiting errors + else if ( + error.message.includes('rate limit') || + error.message.includes('rate_limited') + ) { + console.error( + 'Slack API rate limit exceeded. Please try again later.', + ); + } + // Generic error handling + else { + console.error(`Error in Slack Now Playing: ${error.message}`); + } + } else { + console.error(`Error in Slack Now Playing: ${String(error)}`); + } + }); + } catch (error: unknown) { // Handle unexpected errors in the callback itself if (error instanceof Error) { console.error(`Error processing song info: ${error.message}`, { name: error.name, - stack: error.stack + stack: error.stack, }); } else { console.error(`Error processing song info: ${String(error)}`); @@ -786,7 +905,7 @@ export const backend = createBackend({ try { // Get the latest configuration const latestConfig = await state.context.getConfig(); - const config = latestConfig as SlackNowPlayingConfig; + const config = latestConfig; // Update the stored configuration state.currentConfig = config; @@ -796,10 +915,12 @@ export const backend = createBackend({ // Use assertValidConfig to validate the configuration assertValidConfig(config); // Configuration updated successfully - } catch (error: any) { - console.warn(`Slack Now Playing configuration validation failed: ${error instanceof Error ? error.message : String(error)}`); + } catch (error: unknown) { + console.warn( + `Slack Now Playing configuration validation failed: ${error instanceof Error ? error.message : String(error)}`, + ); } - } catch (error: any) { + } catch (error: unknown) { console.error('Error updating Slack Now Playing configuration:', error); } } diff --git a/src/plugins/slack-now-playing/menu.ts b/src/plugins/slack-now-playing/menu.ts index 10d5df37dd..8ee5135ca5 100644 --- a/src/plugins/slack-now-playing/menu.ts +++ b/src/plugins/slack-now-playing/menu.ts @@ -1,5 +1,6 @@ import prompt from 'custom-electron-prompt'; -import { BrowserWindow, dialog } from 'electron'; +import { type BrowserWindow, dialog } from 'electron'; + import promptOptions from '@/providers/prompt-options'; import { t } from '@/i18n'; @@ -34,19 +35,23 @@ function validateConfig(config: SlackNowPlayingConfig): ValidationResult { if (!config.cookieToken) { errors.push('Missing Slack cookie token'); } else if (!config.cookieToken.startsWith('xoxd-')) { - errors.push('Invalid Slack cookie token format (should start with "xoxd-")'); + errors.push( + 'Invalid Slack cookie token format (should start with "xoxd-")', + ); } // Check emoji name if (!config.emojiName) { errors.push('Missing custom emoji name'); } else if (!/^[a-z0-9_-]+$/.test(config.emojiName)) { - errors.push('Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)'); + errors.push( + 'Invalid emoji name format (should only contain lowercase letters, numbers, hyphens, and underscores)', + ); } return { valid: errors.length === 0, - errors + errors, }; } @@ -141,7 +146,7 @@ async function promptSlackNowPlayingOptions( // Save the config even if it has validation errors // This allows users to save partial configurations - await setConfig(updatedOptions); + setConfig(updatedOptions); // Config has been saved successfully } catch (error) { @@ -157,11 +162,11 @@ async function promptSlackNowPlayingOptions( } } -export const onMenu = async ({ +export const onMenu = ({ window, getConfig, setConfig, -}: MenuContext): Promise => { +}: MenuContext): MenuTemplate => { return [ { label: t('plugins.slack-now-playing.menu.settings'), diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index 82398571bf..a32cd1c35e 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -1,5 +1,7 @@ -import { FormData } from 'formdata-node'; -import https from 'node:https'; +// Using native fetch instead of Electron's net.fetch because: +// 1. net.fetch/session.fetch restrict Cookie headers (Chromium security) +// 2. Native fetch in Node.js 18+ allows Cookie headers +// 3. This runs in the main process where Node.js fetch is available /** * Standard response format from Slack API endpoints @@ -68,7 +70,14 @@ export interface SlackApiResponse { * ``` */ export type SlackApiParams = { - [key: string]: string | number | boolean | null | undefined | string[] | number[]; + [key: string]: + | string + | number + | boolean + | null + | undefined + | string[] + | number[]; }; /** @@ -97,7 +106,7 @@ export class SlackError extends Error { endpoint: string, originalError: Error, statusCode?: number, - responseData?: SlackApiResponse + responseData?: SlackApiResponse, ) { super(message); this.name = 'SlackError'; @@ -155,7 +164,7 @@ export class SlackApiClient { /** Base URL for all Slack API requests */ private readonly baseUrl = 'https://slack.com/api'; /** Cache for GET requests to reduce API calls */ - private readonly cache: Map> = new Map(); + private readonly cache: Map> = new Map(); /** Default cache expiration time (5 minutes) */ private readonly defaultCacheExpiryMs = 5 * 60 * 1000; /** Rate limit tracking for each endpoint */ @@ -204,30 +213,15 @@ export class SlackApiClient { * @returns Headers object with authentication information */ private getBaseHeaders(): Record { + // Note: Token is sent in request body, not Authorization header + // This matches how Slack's internal API expects authentication + // URL-encode the cookie value to handle special characters like + and / + const encodedCookie = encodeURIComponent(this.cookie); return { - 'Cookie': `d=${this.cookie}`, - 'Authorization': `Bearer ${this.token}`, + 'Cookie': `d=${encodedCookie}`, }; } - /** - * Create fetch request options with options for SSL certificate validation - * @param headers HTTP headers to include in the request - * @param options Additional configuration options - * @returns Fetch request init object - */ - private createFetchOptions(headers: Record, options: { disableSSLValidation?: boolean } = {}): RequestInit { - const fetchOptions: RequestInit = { - headers - }; - // For SSL validation disabling, use agent in Node.js - if (options.disableSSLValidation) { - // @ts-ignore - fetchOptions.agent = new https.Agent({ rejectUnauthorized: false }); - } - return fetchOptions; - } - /** * Make a POST request to a Slack API endpoint with rate limiting protection * @@ -242,10 +236,10 @@ export class SlackApiClient { * @returns The response from the API * @throws {SlackError} If the request fails or would exceed rate limits */ - async post( + async post( endpoint: string, - data: Record | FormData, - formData = false + data: Record | FormData, + formData = false, ): Promise> { // Check rate limits before making the request if (!this.checkRateLimit(endpoint)) { @@ -254,23 +248,26 @@ export class SlackApiClient { endpoint, new Error('Too many requests in a short period'), 429, // HTTP 429 Too Many Requests - { ok: false, error: 'rate_limited' } + { ok: false, error: 'rate_limited' }, ); } const url = `${this.baseUrl}/${endpoint}`; - let headers = this.getBaseHeaders(); - let payload: any = data; + const headers = this.getBaseHeaders(); + let payload: BodyInit; - if (formData) { - // Do not set or merge Content-Type headers; fetch will handle this for formdata-node + if (formData && data instanceof FormData) { + // For FormData, add token to the form data + data.append('token', this.token); payload = data; } else { // For regular POST requests, use URL-encoded format headers['Content-Type'] = 'application/x-www-form-urlencoded'; - // Filter out undefined and null values - const cleanData: Record = {}; + // Filter out undefined and null values and add token + const cleanData: Record = { + token: this.token, // Token goes in body, not Authorization header + }; for (const [key, value] of Object.entries(data)) { if (value !== undefined && value !== null) { cleanData[key] = String(value); @@ -284,24 +281,20 @@ export class SlackApiClient { // Update rate limit tracking this.updateRateLimit(endpoint); - // Create request fetchOptions with SSL validation disabled for local development - // Set disableSSLValidation to true to bypass SSL certificate validation - const fetchOptions = this.createFetchOptions(headers, { - disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' + // Use native fetch (Node.js 18+) which allows Cookie headers + const res = await fetch(url, { + method: 'POST', + headers, + body: payload, }); - - fetchOptions.method = 'POST'; - fetchOptions.body = payload; - - const res = await fetch(url, fetchOptions); - const json: SlackApiResponse = await res.json(); + const json = (await res.json()) as SlackApiResponse; if (!json.ok) { if (json.error === 'rate_limited') { const rateLimit = this.rateLimits.get(endpoint) || { requestCount: 0, windowStart: Date.now(), - ...this.defaultRateLimit + ...this.defaultRateLimit, }; rateLimit.requestCount = rateLimit.maxRequests; this.rateLimits.set(endpoint, rateLimit); @@ -311,15 +304,19 @@ export class SlackApiClient { endpoint, new Error(json.error_description || json.error || 'Unknown error'), res.status, - json as SlackApiResponse + json, ); } return json; - } catch (error: any) { + } catch (error: unknown) { + // Re-throw SlackErrors directly to preserve statusCode and responseData + if (error instanceof SlackError) { + throw error; + } throw new SlackError( `Error in Slack API POST to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, endpoint, - error instanceof Error ? error : new Error(String(error)) + error instanceof Error ? error : new Error(String(error)), ); } } @@ -338,7 +335,7 @@ export class SlackApiClient { rateLimit = { requestCount: 0, windowStart: now, - ...this.defaultRateLimit + ...this.defaultRateLimit, }; this.rateLimits.set(endpoint, rateLimit); } @@ -406,7 +403,7 @@ export class SlackApiClient { this.cache.set(cacheKey, { data, timestamp: Date.now(), - expiryMs: expiryMs ?? this.defaultCacheExpiryMs + expiryMs: expiryMs ?? this.defaultCacheExpiryMs, }); } @@ -426,16 +423,19 @@ export class SlackApiClient { * @returns The response from the API * @throws {SlackError} If the request fails or would exceed rate limits */ - async get( + async get( endpoint: string, params: SlackApiParams = {}, - options: { skipCache?: boolean; cacheExpiryMs?: number } = {} + options: { skipCache?: boolean; cacheExpiryMs?: number } = {}, ): Promise> { const url = `${this.baseUrl}/${endpoint}`; const headers = this.getBaseHeaders(); // Remove undefined and null values from params and handle array values - const cleanParams: Record = {}; + const cleanParams: Record< + string, + string | number | boolean | string[] | number[] + > = {}; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { // Handle array values properly @@ -446,7 +446,8 @@ export class SlackApiClient { // Check cache first (unless skipCache is true) if (!options.skipCache) { const cacheKey = this.getCacheKey(endpoint, cleanParams); - const cachedResponse = this.getCachedResponse>(cacheKey); + const cachedResponse = + this.getCachedResponse>(cacheKey); if (cachedResponse) { // Return the cached response @@ -461,13 +462,14 @@ export class SlackApiClient { endpoint, new Error('Too many requests in a short period'), 429, // HTTP 429 Too Many Requests - { ok: false, error: 'rate_limited' } + { ok: false, error: 'rate_limited' }, ); } try { - // Build query string + // Build query string with token const searchParams = new URLSearchParams(); + searchParams.append('token', this.token); // Token goes in query params for (const [key, value] of Object.entries(cleanParams)) { if (Array.isArray(value)) { for (const v of value) { @@ -482,13 +484,12 @@ export class SlackApiClient { // Update rate limit tracking this.updateRateLimit(endpoint); - const fetchOptions = this.createFetchOptions(headers, { - disableSSLValidation: !process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' + // Use native fetch (Node.js 18+) which allows Cookie headers + const res = await fetch(fetchUrl, { + method: 'GET', + headers, }); - fetchOptions.method = 'GET'; - - const res = await fetch(fetchUrl, fetchOptions); - const json: SlackApiResponse = await res.json(); + const json = (await res.json()) as SlackApiResponse; if (!json.ok) { throw new SlackError( @@ -496,7 +497,7 @@ export class SlackApiClient { endpoint, new Error(json.error_description || json.error || 'Unknown error'), res.status, - json as SlackApiResponse + json, ); } @@ -507,11 +508,15 @@ export class SlackApiClient { } return json; - } catch (error: any) { + } catch (error: unknown) { + // Re-throw SlackErrors directly to preserve statusCode and responseData + if (error instanceof SlackError) { + throw error; + } throw new SlackError( `Error in Slack API GET to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, endpoint, - error instanceof Error ? error : new Error(String(error)) + error instanceof Error ? error : new Error(String(error)), ); } } From 71bb0eb4319ec1a9e1ce75e5e421b1be7e3fc738 Mon Sep 17 00:00:00 2001 From: Curtis Gibby Date: Fri, 2 Jan 2026 21:06:00 -0700 Subject: [PATCH 29/29] refactor(slack-now-playing): use Electron session.fetch instead of native fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use session.fromPartition for dedicated Slack API session - Set URL-encoded cookie via session.cookies.set() - Use session.fetch with credentials: 'include' - More consistent with other plugins in the codebase 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../slack-now-playing/slack-api-client.ts | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/src/plugins/slack-now-playing/slack-api-client.ts b/src/plugins/slack-now-playing/slack-api-client.ts index a32cd1c35e..ff98a37371 100644 --- a/src/plugins/slack-now-playing/slack-api-client.ts +++ b/src/plugins/slack-now-playing/slack-api-client.ts @@ -1,7 +1,4 @@ -// Using native fetch instead of Electron's net.fetch because: -// 1. net.fetch/session.fetch restrict Cookie headers (Chromium security) -// 2. Native fetch in Node.js 18+ allows Cookie headers -// 3. This runs in the main process where Node.js fetch is available +import { session } from 'electron'; /** * Standard response format from Slack API endpoints @@ -171,6 +168,8 @@ export class SlackApiClient { private readonly rateLimits: Map = new Map(); /** Default rate limit (20 requests per minute for most endpoints) */ private readonly defaultRateLimit = { maxRequests: 20, windowMs: 60 * 1000 }; + /** Dedicated session for Slack API requests */ + private slackSession: Electron.Session | null = null; /** * Create a new Slack API client @@ -182,6 +181,29 @@ export class SlackApiClient { this.cookie = cookie; } + /** + * Get or create a dedicated session for Slack API requests + * Sets the authentication cookie with proper URL encoding + */ + private async getSlackSession(): Promise { + if (this.slackSession) return this.slackSession; + + this.slackSession = session.fromPartition('persist:slack-api'); + + // Set the URL-encoded cookie in the session + await this.slackSession.cookies.set({ + url: 'https://slack.com', + name: 'd', + value: encodeURIComponent(this.cookie), + domain: '.slack.com', + path: '/', + secure: true, + httpOnly: true, + }); + + return this.slackSession; + } + /** * Clear the response cache and reset rate limits * @@ -210,16 +232,12 @@ export class SlackApiClient { /** * Get the base headers required for all Slack API requests - * @returns Headers object with authentication information + * @returns Headers object (cookie is handled via session) */ private getBaseHeaders(): Record { - // Note: Token is sent in request body, not Authorization header - // This matches how Slack's internal API expects authentication - // URL-encode the cookie value to handle special characters like + and / - const encodedCookie = encodeURIComponent(this.cookie); - return { - 'Cookie': `d=${encodedCookie}`, - }; + // Cookie is set via session.cookies.set() - credentials: 'include' sends it + // Token is sent in request body, not Authorization header + return {}; } /** @@ -278,14 +296,18 @@ export class SlackApiClient { } try { + // Get the Slack session with cookie already set + const slackSession = await this.getSlackSession(); + // Update rate limit tracking this.updateRateLimit(endpoint); - // Use native fetch (Node.js 18+) which allows Cookie headers - const res = await fetch(url, { + // Use session.fetch with credentials: 'include' to send session cookies + const res = await slackSession.fetch(url, { method: 'POST', headers, body: payload, + credentials: 'include', }); const json = (await res.json()) as SlackApiResponse; @@ -481,13 +503,17 @@ export class SlackApiClient { } const fetchUrl = `${url}?${searchParams.toString()}`; + // Get the Slack session with cookie already set + const slackSession = await this.getSlackSession(); + // Update rate limit tracking this.updateRateLimit(endpoint); - // Use native fetch (Node.js 18+) which allows Cookie headers - const res = await fetch(fetchUrl, { + // Use session.fetch with credentials: 'include' to send session cookies + const res = await slackSession.fetch(fetchUrl, { method: 'GET', headers, + credentials: 'include', }); const json = (await res.json()) as SlackApiResponse;