From 8fa49f298f8c12a7b5acd8acdbcdcfb51f69a351 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 15:56:28 +0300 Subject: [PATCH 01/17] debugging e2e --- test/e2e-playwright/playwright.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 04747ee77..bb181d9ee 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -21,7 +21,7 @@ export default defineConfig({ use: { baseURL, screenshot: 'only-on-failure', - trace: 'on-first-retry', + trace: 'on', video: 'retain-on-failure', }, webServer: { From 7425392413ef72c718962b9a8ca7f195f5a39509 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 16:01:39 +0300 Subject: [PATCH 02/17] Revert "debugging e2e" This reverts commit 8fa49f298f8c12a7b5acd8acdbcdcfb51f69a351. --- test/e2e-playwright/playwright.config.mjs | 2 +- test/e2e-playwright/specs/admin_smoke.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index bb181d9ee..04747ee77 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -21,7 +21,7 @@ export default defineConfig({ use: { baseURL, screenshot: 'only-on-failure', - trace: 'on', + trace: 'on-first-retry', video: 'retain-on-failure', }, webServer: { diff --git a/test/e2e-playwright/specs/admin_smoke.spec.js b/test/e2e-playwright/specs/admin_smoke.spec.js index 01fd38b29..7ccb73557 100644 --- a/test/e2e-playwright/specs/admin_smoke.spec.js +++ b/test/e2e-playwright/specs/admin_smoke.spec.js @@ -22,6 +22,6 @@ test('admin can open the admin settings modal', async ({ page }) => { modal.getByRole('heading', { name: 'Administration' }), ).toBeVisible() - await modal.getByRole('tab', { name: 'Emoji' }).click() + await modal.getByRole('tab', { title: 'Emoji' }).click() await expect(modal.getByText('Emoji packs')).toBeVisible() }) From 329c4c18a75a76f78304f5e1d84f9866923a0b68 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 16:03:07 +0300 Subject: [PATCH 03/17] debugging --- test/e2e-playwright/specs/user_smoke.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/e2e-playwright/specs/user_smoke.spec.js b/test/e2e-playwright/specs/user_smoke.spec.js index 205cd3e69..74945ed70 100644 --- a/test/e2e-playwright/specs/user_smoke.spec.js +++ b/test/e2e-playwright/specs/user_smoke.spec.js @@ -55,11 +55,14 @@ const login = async (page, user) => { test('user can register, log out, and log back in', async ({ page }) => { const user = createTestUser() await register(page, user) + console.debug('Register Success') await expect(page.getByTitle('Log out')).toBeVisible() await logout(page) + console.debug('Logout Success') await login(page, user) + console.debug('Login Success') await expect(page.getByTitle('Log out')).toBeVisible() }) From 0b27da21ace179b241b74d33fbe43efcc5198dfa Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 16:22:13 +0300 Subject: [PATCH 04/17] didn't help at all --- test/e2e-playwright/specs/admin_smoke.spec.js | 2 +- test/e2e-playwright/specs/user_smoke.spec.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/e2e-playwright/specs/admin_smoke.spec.js b/test/e2e-playwright/specs/admin_smoke.spec.js index 7ccb73557..01fd38b29 100644 --- a/test/e2e-playwright/specs/admin_smoke.spec.js +++ b/test/e2e-playwright/specs/admin_smoke.spec.js @@ -22,6 +22,6 @@ test('admin can open the admin settings modal', async ({ page }) => { modal.getByRole('heading', { name: 'Administration' }), ).toBeVisible() - await modal.getByRole('tab', { title: 'Emoji' }).click() + await modal.getByRole('tab', { name: 'Emoji' }).click() await expect(modal.getByText('Emoji packs')).toBeVisible() }) diff --git a/test/e2e-playwright/specs/user_smoke.spec.js b/test/e2e-playwright/specs/user_smoke.spec.js index 74945ed70..205cd3e69 100644 --- a/test/e2e-playwright/specs/user_smoke.spec.js +++ b/test/e2e-playwright/specs/user_smoke.spec.js @@ -55,14 +55,11 @@ const login = async (page, user) => { test('user can register, log out, and log back in', async ({ page }) => { const user = createTestUser() await register(page, user) - console.debug('Register Success') await expect(page.getByTitle('Log out')).toBeVisible() await logout(page) - console.debug('Logout Success') await login(page, user) - console.debug('Login Success') await expect(page.getByTitle('Log out')).toBeVisible() }) From ab34412a2ede8783aa70f43e42150000d0014783 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 18:26:01 +0300 Subject: [PATCH 05/17] should fix admin e2e test --- docker-compose.e2e.yml | 2 ++ src/api/helpers.js | 1 + src/stores/admin_settings.js | 23 ++++++++++------------- test/e2e-playwright/playwright.config.mjs | 6 ++++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 75a4979a1..d97fb6bef 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -39,6 +39,8 @@ services: interval: 5s timeout: 3s retries: 60 + ports: + - 4000:4000 e2e: build: diff --git a/src/api/helpers.js b/src/api/helpers.js index a7e1b624b..50841a495 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -132,6 +132,7 @@ export const promisedRequest = async ({ ) } } catch (error) { + if (error.name === 'StatusCodeError') throw error throw new Error(error, { url, options }, response) } } diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index 06bcab9ae..44146756f 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -87,22 +87,19 @@ export const useAdminSettingsStore = defineStore('adminSettings', { loadAdminStuff() { getInstanceDBConfig({ credentials: useOAuthStore().token, - }).then(({ data: backendDbConfig }) => { - if (backendDbConfig.error) { - if (backendDbConfig.error.status === 400) { - backendDbConfig.error.json().then((errorJson) => { - if (/configurable_from_database/.test(errorJson.error)) { - this.setInstanceAdminNoDbConfig() - } - }) - } - } else { + }) + .then(({ data: backendDbConfig }) => this.setInstanceAdminSettings({ credentials: useOAuthStore().token, backendDbConfig, - }) - } - }) + })) + .catch(({ statusCode, statusText }) => { + if (statusCode === 400) { + if (/configurable_from_database/.test(statusText)) { + this.setInstanceAdminNoDbConfig() + } + } + }) if (this.descriptions === null) { getInstanceConfigDescriptions({ credentials: useOAuthStore().token, diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 04747ee77..00bc15723 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -1,7 +1,7 @@ /* global process */ import { defineConfig, devices } from 'playwright/test' -const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8080' +const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8099' export default defineConfig({ testDir: './specs', @@ -25,12 +25,14 @@ export default defineConfig({ video: 'retain-on-failure', }, webServer: { - command: 'yarn dev -- --host 0.0.0.0 --port 8080 --strictPort', + command: 'yarn dev -- --host 0.0.0.0 --strictPort', url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { ...process.env, + PORT: + process.env.PORT || '8099', VITE_PROXY_TARGET: process.env.VITE_PROXY_TARGET || 'http://localhost:4000', VITE_PROXY_ORIGIN: From 8c7369e15d9cca492c97283e400627e6b12924bb Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 19:56:07 +0300 Subject: [PATCH 06/17] port? --- test/e2e-playwright/playwright.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 00bc15723..0120b5b58 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -25,7 +25,7 @@ export default defineConfig({ video: 'retain-on-failure', }, webServer: { - command: 'yarn dev -- --host 0.0.0.0 --strictPort', + command: 'yarn dev -- --host 0.0.0.0 --port $PORT --strictPort', url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, From 0e4222c02b05dc0e2007a6d061a78405d367ea78 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 20:01:36 +0300 Subject: [PATCH 07/17] use port 8099 --- .gitlab-ci.yml | 2 +- .woodpecker/test-e2e.yaml | 2 +- docker-compose.e2e.yml | 2 +- test/e2e/specs/test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 247218091..692e71fd5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -175,7 +175,7 @@ e2e-pleroma: NOTIFY_EMAIL: $E2E_ADMIN_EMAIL VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 script: - npm install -g yarn@1.22.22 - yarn --frozen-lockfile diff --git a/.woodpecker/test-e2e.yaml b/.woodpecker/test-e2e.yaml index c0bf103cd..a5289bd78 100644 --- a/.woodpecker/test-e2e.yaml +++ b/.woodpecker/test-e2e.yaml @@ -30,7 +30,7 @@ steps: environment: APT_CACHE_DIR: apt-cache DEBIAN_FRONTEND: noninteractive - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 FF_NETWORK_PER_BUILD: "true" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" VITE_PROXY_ORIGIN: "http://pleroma:4000" diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index d97fb6bef..c49984684 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -53,7 +53,7 @@ services: CI: "1" VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} command: ["yarn", "e2e:pw"] diff --git a/test/e2e/specs/test.js b/test/e2e/specs/test.js index f8993989b..ba3e757fd 100644 --- a/test/e2e/specs/test.js +++ b/test/e2e/specs/test.js @@ -4,7 +4,7 @@ module.exports = { 'default e2e tests': function (browser) { // automatically uses dev Server port from /config.index.js - // default: http://localhost:8080 + // default: http://localhost:8099 // see nightwatch.conf.js const devServer = browser.globals.devServerURL From cb20672f0b4a2f7115a4b1fe12311158dcabdd2f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 20:07:44 +0300 Subject: [PATCH 08/17] lint --- src/stores/admin_settings.js | 3 ++- test/e2e-playwright/playwright.config.mjs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index 44146756f..5aa91c819 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -92,7 +92,8 @@ export const useAdminSettingsStore = defineStore('adminSettings', { this.setInstanceAdminSettings({ credentials: useOAuthStore().token, backendDbConfig, - })) + }), + ) .catch(({ statusCode, statusText }) => { if (statusCode === 400) { if (/configurable_from_database/.test(statusText)) { diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 0120b5b58..51a4de21e 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -31,8 +31,7 @@ export default defineConfig({ timeout: 120_000, env: { ...process.env, - PORT: - process.env.PORT || '8099', + PORT: process.env.PORT || '8099', VITE_PROXY_TARGET: process.env.VITE_PROXY_TARGET || 'http://localhost:4000', VITE_PROXY_ORIGIN: From ee42d4095a70ab774203af401514c9d6ac81f22d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jun 2026 20:15:47 +0300 Subject: [PATCH 09/17] wrong fetcher --- src/stores/bookmark_folders.js | 4 ++-- src/stores/lists.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stores/bookmark_folders.js b/src/stores/bookmark_folders.js index 6ffec807b..713a21d00 100644 --- a/src/stores/bookmark_folders.js +++ b/src/stores/bookmark_folders.js @@ -27,8 +27,8 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { }, actions: { startFetching() { - promiseInterval(() => { - this.fetcher = fetchBookmarkFolders({ + this.fetcher = promiseInterval(() => { + fetchBookmarkFolders({ credentials: useOAuthStore().token, }) .then(({ data: folders }) => this.setBookmarkFolders(folders)) diff --git a/src/stores/lists.js b/src/stores/lists.js index 7634f1061..37471d46e 100644 --- a/src/stores/lists.js +++ b/src/stores/lists.js @@ -34,8 +34,8 @@ export const useListsStore = defineStore('lists', { }, actions: { startFetching() { - promiseInterval(() => { - this.fetcher = fetchLists({ + this.fetcher = promiseInterval(() => { + fetchLists({ credentials: useOAuthStore().token, }) .then(({ data: lists }) => this.setLists(lists)) From ada7ae65adb8645a4495052621209b3a5425391a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 18:50:56 +0300 Subject: [PATCH 10/17] don't rethrow --- src/api/helpers.js | 62 +++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/api/helpers.js b/src/api/helpers.js index 50841a495..6986a6ab7 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -98,42 +98,36 @@ export const promisedRequest = async ({ } } - let response = null - try { - response = await fetch(url, options) - const data = await (async () => { - const [contentType] = response.headers - .get('content-type') - .split(';') - .map((x) => x.toLowerCase().trim()) - const contentLength = parseInt(response.headers.get('content-length')) - if (contentLength === 0) return null + const response = await fetch(url, options) + const data = await (async () => { + const [contentType] = response.headers + .get('content-type') + .split(';') + .map((x) => x.toLowerCase().trim()) + const contentLength = parseInt(response.headers.get('content-length')) + if (contentLength === 0) return null - switch (contentType) { - case 'text/plain': - return await response.text() - case 'application/json': - return await response.json() - default: - return await response.bytes() - } - })() - - const { ok, status } = response - - if (ok) { - return { response, status, data } - } else { - throw new StatusCodeError( - response.status, - data, - { url, options }, - response, - ) + switch (contentType) { + case 'text/plain': + return await response.text() + case 'application/json': + return await response.json() + default: + return await response.bytes() } - } catch (error) { - if (error.name === 'StatusCodeError') throw error - throw new Error(error, { url, options }, response) + })() + + const { ok, status } = response + + if (ok) { + return { response, status, data } + } else { + throw new StatusCodeError( + response.status, + data, + { url, options }, + response, + ) } } From 30db636389654c74fb7f1e6b062e9ad7c322435a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:03:46 +0300 Subject: [PATCH 11/17] fix tests --- src/api/oauth.js | 2 -- src/services/errors/errors.js | 2 +- test/unit/specs/stores/oauth.spec.js | 6 ++++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/api/oauth.js b/src/api/oauth.js index 7ecb0cc3f..b774b3f6c 100644 --- a/src/api/oauth.js +++ b/src/api/oauth.js @@ -2,8 +2,6 @@ import { reduce } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' -import { StatusCodeError } from 'src/services/errors/errors.js' - const REDIRECT_URI = `${window.location.origin}/oauth-callback` export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials' diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 00dfd3712..a28f31775 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -13,7 +13,7 @@ function humanizeErrors(errors) { export function StatusCodeError(statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode - this.statusText = body.error.error || body.error + this.statusText = body.error || body this.details = JSON && JSON.stringify ? JSON.stringify(body) : body this.errorData = body.error this.message = this.statusCode + ' - ' + this.statusText diff --git a/test/unit/specs/stores/oauth.spec.js b/test/unit/specs/stores/oauth.spec.js index c4f4ba978..828175c9f 100644 --- a/test/unit/specs/stores/oauth.spec.js +++ b/test/unit/specs/stores/oauth.spec.js @@ -69,6 +69,12 @@ describe('oauth store', () => { it('should use create an app and record client id and secret', async ({ worker, }) => { + worker.use( + http.post(MASTODON_APP_URL, () => { + return HttpResponse.text('Throttled', { status: 429 }) + }), + ) + const store = useOAuthStore() worker.use(...authApis()) const app = await store.createApp() From 6060d4efbcd57f18e58ec598f62337bfdc255d37 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:03:50 +0300 Subject: [PATCH 12/17] update playwright --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index cdeefc30c..f594d7a99 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "msw": "2.14.6", "nightwatch": "3.12.2", "oxc": "^1.0.1", - "playwright": "1.57.0", + "playwright": "1.61.0", "postcss": "8.5.6", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.6", diff --git a/yarn.lock b/yarn.lock index b8c3e3293..b60f88b7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7289,17 +7289,17 @@ pkg-types@^1.3.1: mlly "^1.7.4" pathe "^2.0.1" -playwright-core@1.57.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" - integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== +playwright-core@1.61.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.61.0.tgz#caf8078b2a39cd7738dc75ec11cb3b47f385c3f0" + integrity sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA== -playwright@1.57.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" - integrity sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw== +playwright@1.61.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.61.0.tgz#7082df3df08ffa82b11420ea5fae84a40bd16e3f" + integrity sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ== dependencies: - playwright-core "1.57.0" + playwright-core "1.61.0" optionalDependencies: fsevents "2.3.2" From 728ba716b81418afd14a144296637ff4cc2efae0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:08:36 +0300 Subject: [PATCH 13/17] lint --- src/api/helpers.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/api/helpers.js b/src/api/helpers.js index 6986a6ab7..5fb11a183 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -122,12 +122,7 @@ export const promisedRequest = async ({ if (ok) { return { response, status, data } } else { - throw new StatusCodeError( - response.status, - data, - { url, options }, - response, - ) + throw new StatusCodeError(response.status, data, { url, options }, response) } } From 354f35c690b996db1d3297674dc2dc2cb3955078 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:38:39 +0300 Subject: [PATCH 14/17] fix and refactor websocket --- src/api/public.js | 164 -------------------- src/components/chat/chat.js | 4 +- src/components/conversation/conversation.js | 4 +- src/modules/api.js | 6 +- vite.config.js | 21 +-- 5 files changed, 22 insertions(+), 177 deletions(-) diff --git a/src/api/public.js b/src/api/public.js index 12af848d7..f4ae778bc 100644 --- a/src/api/public.js +++ b/src/api/public.js @@ -143,8 +143,6 @@ const MASTODON_SEARCH_2 = ({ `/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}` const MASTODON_USER_SEARCH_URL = ({ q, resolve }) => `/api/v1/accounts/search${paramsString({ q, resolve })}` -const MASTODON_STREAMING = ({ accessToken, stream }) => - `/api/v1/streaming${paramsString({ accessToken, stream })}` const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const PLEROMA_EMOJI_REACTIONS_URL = (id) => `/api/v1/pleroma/statuses/${id}/reactions` @@ -487,168 +485,6 @@ export const search2 = ({ export const fetchKnownDomains = ({ credentials }) => promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) -export const getMastodonSocketURI = ({ credentials, stream }, base) => { - return base + MASTODON_STREAMING({ accessToken: credentials, stream }) -} - -const MASTODON_STREAMING_EVENTS = new Set([ - 'update', - 'notification', - 'delete', - 'filters_changed', - 'status.update', -]) - -const PLEROMA_STREAMING_EVENTS = new Set([ - 'pleroma:chat_update', - 'pleroma:respond', -]) - -// A thin wrapper around WebSocket API that allows adding a pre-processor to it -// Uses EventTarget and a CustomEvent to proxy events -export const ProcessedWS = ({ - url, - preprocessor = handleMastoWS, - id = 'Unknown', - credentials, -}) => { - const eventTarget = new EventTarget() - const socket = new WebSocket(url) - if (!socket) throw new Error(`Failed to create socket ${id}`) - const proxy = (original, eventName, processor = (a) => a) => { - original.addEventListener(eventName, (eventData) => { - eventTarget.dispatchEvent( - new CustomEvent(eventName, { detail: processor(eventData) }), - ) - }) - } - socket.addEventListener('open', (wsEvent) => { - console.debug(`[WS][${id}] Socket connected`, wsEvent) - if (credentials) { - socket.send( - JSON.stringify({ - type: 'pleroma:authenticate', - token: credentials, - }), - ) - } - }) - socket.addEventListener('error', (wsEvent) => { - console.debug(`[WS][${id}] Socket errored`, wsEvent) - }) - socket.addEventListener('close', (wsEvent) => { - console.debug( - `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, - wsEvent, - ) - }) - // Commented code reason: very spammy, uncomment to enable message debug logging - /* - socket.addEventListener('message', (wsEvent) => { - console.debug( - `[WS][${id}] Message received`, - wsEvent - ) - }) - /**/ - - const onAuthenticated = () => { - eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) - } - - proxy(socket, 'open') - proxy(socket, 'close') - proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) - proxy(socket, 'error') - - // 1000 = Normal Closure - eventTarget.close = () => { - socket.close(1000, 'Shutting down socket') - } - eventTarget.getState = () => socket.readyState - eventTarget.subscribe = (stream, args = {}) => { - console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) - socket.send( - JSON.stringify({ - type: 'subscribe', - stream, - ...args, - }), - ) - } - eventTarget.unsubscribe = (stream, args = {}) => { - console.debug( - `[WS][${id}] Unsubscribing from stream ${stream} with args`, - args, - ) - socket.send( - JSON.stringify({ - type: 'unsubscribe', - stream, - ...args, - }), - ) - } - - return eventTarget -} - -export const handleMastoWS = ( - wsEvent, - { - onAuthenticated = () => { - /* no-op */ - }, - } = {}, -) => { - const { data } = wsEvent - if (!data) return - const parsedEvent = JSON.parse(data) - const { event, payload } = parsedEvent - if ( - MASTODON_STREAMING_EVENTS.has(event) || - PLEROMA_STREAMING_EVENTS.has(event) - ) { - // MastoBE and PleromaBE both send payload for delete as a PLAIN string - if (event === 'delete') { - return { event, id: payload } - } - const data = payload ? JSON.parse(payload) : null - if (event === 'pleroma:respond') { - if (data.type === 'pleroma:authenticate') { - if (data.result === 'success') { - console.debug('[WS] Successfully authenticated') - onAuthenticated() - } else { - console.error('[WS] Unable to authenticate:', data.error) - wsEvent.target.close() - } - } - return null - } else if (event === 'update') { - return { event, status: parseStatus(data) } - } else if (event === 'status.update') { - return { event, status: parseStatus(data) } - } else if (event === 'notification') { - return { event, notification: parseNotification(data) } - } else if (event === 'pleroma:chat_update') { - return { event, chatUpdate: parseChat(data) } - } - } else { - console.warn('Unknown event', wsEvent) - return null - } -} - -export const WSConnectionStatus = Object.freeze({ - JOINED: 1, - CLOSED: 2, - ERROR: 3, - DISABLED: 4, - STARTING: 5, - STARTING_INITIAL: 6, -}) - export const fetchScrobbles = ({ accountId, limit = 1 }) => promisedRequest({ url: PLEROMA_SCROBBLES_URL(accountId, { limit }), diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 68306acbe..acd1f1c5f 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -23,7 +23,9 @@ import { getOrCreateChat, sendChatMessage, } from 'src/api/chats.js' -import { WSConnectionStatus } from 'src/api/public.js' +import { + WSConnectionStatus, +} from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 281f2b573..af78b7383 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -13,8 +13,10 @@ import { useOAuthStore } from 'src/stores/oauth.js' import { fetchConversation, fetchStatus, - WSConnectionStatus, } from 'src/api/public.js' +import { + WSConnectionStatus, +} from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/modules/api.js b/src/modules/api.js index a36e21c05..9d6426c2e 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -10,10 +10,12 @@ import { useShoutStore } from 'src/stores/shout.js' import { fetchTimeline, +} from 'src/api/public.js' +import { getMastodonSocketURI, ProcessedWS, WSConnectionStatus, -} from 'src/api/public.js' +} from 'src/api/websocket.js' import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service' import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js' import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js' @@ -99,7 +101,7 @@ const api = { const serv = useInstanceStore().server.replace('http', 'ws') const credentials = useOAuthStore().token - const url = getMastodonSocketURI({ credentials }, serv) + const url = getMastodonSocketURI({ credentials }) state.mastoUserSocket = ProcessedWS({ url, diff --git a/vite.config.js b/vite.config.js index 6cbba4a53..4c7b4f534 100644 --- a/vite.config.js +++ b/vite.config.js @@ -73,6 +73,7 @@ export default defineConfig(async ({ mode, command }) => { const settings = await getLocalDevSettings() const target = settings.target || 'http://localhost:4000/' const origin = settings.origin || target + const targetSW = target.replace(/^http/,'ws') const transformSW = getTransformSWSettings(settings) const proxy = { '/api': { @@ -80,6 +81,7 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', ws: true, + rewriteWsOrigin: true, }, '/auth': { // Mastodon password reset lives here @@ -98,20 +100,21 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', }, - '/socket': { - target, - changeOrigin: true, - cookieDomainRewrite: 'localhost', - ws: true, - headers: { - Origin: origin, - }, - }, '/oauth': { target, changeOrigin: true, cookieDomainRewrite: 'localhost', }, + '/socket': { + target: targetSW, + changeOrigin: true, + cookieDomainRewrite: 'localhost', + rewriteWsOrigin: true, + ws: true, + headers: { + Origin: origin, + }, + }, } const swSrc = 'src/sw.js' From e7e615565c75641090e6541784ef6af9515c4911 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:41:28 +0300 Subject: [PATCH 15/17] lint --- src/api/public.js | 2 -- src/components/chat/chat.js | 4 +--- src/components/conversation/conversation.js | 9 ++------- src/modules/api.js | 5 +---- vite.config.js | 2 +- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/api/public.js b/src/api/public.js index f4ae778bc..cdb3a1305 100644 --- a/src/api/public.js +++ b/src/api/public.js @@ -3,8 +3,6 @@ import { concat, each, last, map } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' import { - parseAttachment, - parseChat, parseLinkHeaderPagination, parseNotification, parseSource, diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index acd1f1c5f..16a03ab1d 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -23,9 +23,7 @@ import { getOrCreateChat, sendChatMessage, } from 'src/api/chats.js' -import { - WSConnectionStatus, -} from 'src/api/websocket.js' +import { WSConnectionStatus } from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index af78b7383..54bcbd373 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -10,13 +10,8 @@ import { useInterfaceStore } from 'src/stores/interface' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' -import { - fetchConversation, - fetchStatus, -} from 'src/api/public.js' -import { - WSConnectionStatus, -} from 'src/api/websocket.js' +import { fetchConversation, fetchStatus } from 'src/api/public.js' +import { WSConnectionStatus } from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/modules/api.js b/src/modules/api.js index 9d6426c2e..ca1aacf05 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -8,9 +8,7 @@ import { useInterfaceStore } from 'src/stores/interface.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useShoutStore } from 'src/stores/shout.js' -import { - fetchTimeline, -} from 'src/api/public.js' +import { fetchTimeline } from 'src/api/public.js' import { getMastodonSocketURI, ProcessedWS, @@ -99,7 +97,6 @@ const api = { const { state, commit, dispatch, rootState } = store const timelineData = rootState.statuses.timelines.friends - const serv = useInstanceStore().server.replace('http', 'ws') const credentials = useOAuthStore().token const url = getMastodonSocketURI({ credentials }) diff --git a/vite.config.js b/vite.config.js index 4c7b4f534..64859ebb1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -73,7 +73,7 @@ export default defineConfig(async ({ mode, command }) => { const settings = await getLocalDevSettings() const target = settings.target || 'http://localhost:4000/' const origin = settings.origin || target - const targetSW = target.replace(/^http/,'ws') + const targetSW = target.replace(/^http/, 'ws') const transformSW = getTransformSWSettings(settings) const proxy = { '/api': { From e80b049904ed7276e192dfdd9e5bc711c1ae7219 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:42:54 +0300 Subject: [PATCH 16/17] lightening --- src/api/public.js | 2 -- src/api/user.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/api/public.js b/src/api/public.js index cdb3a1305..bd3f56fce 100644 --- a/src/api/public.js +++ b/src/api/public.js @@ -1,5 +1,3 @@ -import { concat, each, last, map } from 'lodash' - import { paramsString, promisedRequest } from './helpers.js' import { diff --git a/src/api/user.js b/src/api/user.js index 127debdea..ce41c0c21 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,4 @@ -import { concat, each, last, map } from 'lodash' +import { concat, last } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' import { fetchFriends, MASTODON_STATUS_URL } from './public.js' From aaa3f64a0252d0bc19cb73674bdb7a5b2374f879 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 24 Jun 2026 19:43:58 +0300 Subject: [PATCH 17/17] missing file --- src/api/websocket.js | 176 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/api/websocket.js diff --git a/src/api/websocket.js b/src/api/websocket.js new file mode 100644 index 000000000..44f3d391a --- /dev/null +++ b/src/api/websocket.js @@ -0,0 +1,176 @@ +import { paramsString, promisedRequest } from './helpers.js' + +import { + parseChat, + parseNotification, + parseStatus, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_STREAMING = ({ accessToken, stream }) => + `/api/v1/streaming${paramsString({ accessToken, stream })}` + +export const getMastodonSocketURI = ({ credentials, stream }) => { + return MASTODON_STREAMING({ accessToken: credentials, stream }) +} + +const MASTODON_STREAMING_EVENTS = new Set([ + 'update', + 'notification', + 'delete', + 'filters_changed', + 'status.update', +]) + +const PLEROMA_STREAMING_EVENTS = new Set([ + 'pleroma:chat_update', + 'pleroma:respond', +]) + +// A thin wrapper around WebSocket API that allows adding a pre-processor to it +// Uses EventTarget and a CustomEvent to proxy events +export const ProcessedWS = ({ + url, + preprocessor = handleMastoWS, + id = 'Unknown', + credentials, +}) => { + const eventTarget = new EventTarget() + const socket = new WebSocket(url) + if (!socket) throw new Error(`Failed to create socket ${id}`) + const proxy = (original, eventName, processor = (a) => a) => { + original.addEventListener(eventName, (eventData) => { + eventTarget.dispatchEvent( + new CustomEvent(eventName, { detail: processor(eventData) }), + ) + }) + } + socket.addEventListener('open', (wsEvent) => { + console.debug(`[WS][${id}] Socket connected`, wsEvent) + if (credentials) { + socket.send( + JSON.stringify({ + type: 'pleroma:authenticate', + token: credentials, + }), + ) + } + }) + socket.addEventListener('error', (wsEvent) => { + console.debug(`[WS][${id}] Socket errored`, wsEvent) + }) + socket.addEventListener('close', (wsEvent) => { + console.debug( + `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, + wsEvent, + ) + }) + // Commented code reason: very spammy, uncomment to enable message debug logging + /* + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + /**/ + + const onAuthenticated = () => { + eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) + } + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) + proxy(socket, 'error') + + // 1000 = Normal Closure + eventTarget.close = () => { + socket.close(1000, 'Shutting down socket') + } + eventTarget.getState = () => socket.readyState + eventTarget.subscribe = (stream, args = {}) => { + console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) + socket.send( + JSON.stringify({ + type: 'subscribe', + stream, + ...args, + }), + ) + } + eventTarget.unsubscribe = (stream, args = {}) => { + console.debug( + `[WS][${id}] Unsubscribing from stream ${stream} with args`, + args, + ) + socket.send( + JSON.stringify({ + type: 'unsubscribe', + stream, + ...args, + }), + ) + } + + return eventTarget +} + +export const handleMastoWS = ( + wsEvent, + { + onAuthenticated = () => { + /* no-op */ + }, + } = {}, +) => { + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if ( + MASTODON_STREAMING_EVENTS.has(event) || + PLEROMA_STREAMING_EVENTS.has(event) + ) { + // MastoBE and PleromaBE both send payload for delete as a PLAIN string + if (event === 'delete') { + return { event, id: payload } + } + const data = payload ? JSON.parse(payload) : null + if (event === 'pleroma:respond') { + if (data.type === 'pleroma:authenticate') { + if (data.result === 'success') { + console.debug('[WS] Successfully authenticated') + onAuthenticated() + } else { + if (data.error === 'already_authenticated') { + onAuthenticated() + } else { + console.error('[WS] Unable to authenticate:', data.error) + wsEvent.target.close() + } + } + } + return null + } else if (event === 'update') { + return { event, status: parseStatus(data) } + } else if (event === 'status.update') { + return { event, status: parseStatus(data) } + } else if (event === 'notification') { + return { event, notification: parseNotification(data) } + } else if (event === 'pleroma:chat_update') { + return { event, chatUpdate: parseChat(data) } + } + } else { + console.warn('Unknown event', wsEvent) + return null + } +} + +export const WSConnectionStatus = Object.freeze({ + JOINED: 1, + CLOSED: 2, + ERROR: 3, + DISABLED: 4, + STARTING: 5, + STARTING_INITIAL: 6, +})