diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4c8d0fb27..e2946b162 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -73,7 +73,7 @@ test: e2e-pleroma: stage: test - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy services: - name: postgres:15-alpine alias: db @@ -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..2a7d1511d 100644 --- a/.woodpecker/test-e2e.yaml +++ b/.woodpecker/test-e2e.yaml @@ -25,12 +25,12 @@ variables: steps: test: - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy entrypoint: *script_file_entrypoint 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/.woodpecker/test.yaml b/.woodpecker/test.yaml index acc48aacc..89f6f2f93 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -25,7 +25,7 @@ variables: steps: test: - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy environment: APT_CACHE_DIR: apt-cache DEBIAN_FRONTEND: noninteractive diff --git a/biome.json b/biome.json index 6a464a0e5..5ef6f79ee 100644 --- a/biome.json +++ b/biome.json @@ -46,6 +46,7 @@ "noUnusedLabels": "error", "noUnusedPrivateClassMembers": "error", "noUnusedVariables": "error", + "noUnusedImports": "error", "useIsNan": "error", "useValidForDirection": "error", "useValidTypeof": "error", diff --git a/build/sw_plugin.js b/build/sw_plugin.js index 066c8da12..2f0a4819d 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -1,4 +1,3 @@ -import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { exactRegex } from '@rolldown/pluginutils' @@ -102,8 +101,12 @@ export const buildSwPlugin = ({ swSrc, swDest }) => { }, }) - const swBundle = await build(config) - return swBundle.output[0] + try { + const swBundle = await build(config) + return swBundle.output[0] + } catch (e) { + console.error('Error building ServiceWorker:', e) + } }, }, closeBundle: { @@ -112,7 +115,11 @@ export const buildSwPlugin = ({ swSrc, swDest }) => { async handler() { if (process.env.VITEST) return console.info('Building service worker for production') - await build(config) + try { + await build(config) + } catch (e) { + console.error('Error building ServiceWorker:', e) + } }, }, } diff --git a/changelog.d/api-refactor.skip b/changelog.d/api-refactor.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 114c22f7f..c740e69c4 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: @@ -51,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/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e index e84359ceb..7e3fbfbf1 100644 --- a/docker/e2e/Dockerfile.e2e +++ b/docker/e2e/Dockerfile.e2e @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.57.0-jammy +FROM mcr.microsoft.com/playwright:v1.61.0-jammy WORKDIR /app diff --git a/package.json b/package.json index c51d98b81..f594d7a99 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,9 @@ "@vitejs/devtools": "^0.3.1", "@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue-jsx": "^5.1.5", - "@vitest/browser": "^3.0.7", - "@vitest/ui": "^3.0.7", + "@vitest/browser-playwright": "^4.1.7", + "@vitest/browser": "^4.1.7", + "@vitest/ui": "^4.1.7", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.5.0", "@vue/compiler-sfc": "3.5.22", @@ -95,10 +96,10 @@ "http-proxy-middleware": "3.0.5", "iso-639-1": "3.1.5", "lodash": "4.17.21", - "msw": "2.10.5", + "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", @@ -118,7 +119,7 @@ "vite": "^8.0.0", "vite-plugin-eslint2": "^5.1.0", "vite-plugin-stylelint": "^6.1.0", - "vitest": "^3.0.7", + "vitest": "^4.1.7", "vue-eslint-parser": "10.2.0" }, "type": "module", diff --git a/src/App.js b/src/App.js index ea2271a75..7de7a0696 100644 --- a/src/App.js +++ b/src/App.js @@ -21,9 +21,6 @@ import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useShoutStore } from 'src/stores/shout.js' -import messages from 'src/i18n/messages' -import localeService from 'src/services/locale/locale.service.js' - // Helper to unwrap reactive proxies window.toValue = (x) => JSON.parse(JSON.stringify(x)) diff --git a/src/api/admin.js b/src/api/admin.js new file mode 100644 index 000000000..67fb5038b --- /dev/null +++ b/src/api/admin.js @@ -0,0 +1,491 @@ +import { promisedRequest } from './helpers.js' + +const REPORTS = '/api/v1/pleroma/admin/reports' +const CONFIG_URL = '/api/v1/pleroma/admin/config' +const DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions' + +const ANNOUNCEMENTS_URL = (id = '') => + `/api/v1/pleroma/admin/announcements/${id}` + +const FRONTENDS_URL = '/api/v1/pleroma/admin/frontends' +const FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install' + +const USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}` +const USERS_URL_LIST = ({ + page, + pageSize, + filters = {}, + query = '', + name = '', + email = '', +}) => { + const { + local = false, + external = false, + active = false, + needApproval = false, + unconfirmed = false, + deactivated = false, + isAdmin = true, + isModerator = true, + } = filters + const filters_str = [ + local && 'local', + external && 'external', + active && 'active', + needApproval && 'need_approval', + unconfirmed && 'unconfirmed', + deactivated && 'deactivated', + isAdmin && 'is_admin', + isModerator && 'is_moderator', + ] + .filter((x) => x) + .join(',') + return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}` +} + +const TAG_USER_URL = '/api/pleroma/admin/users/tag' + +const PERMISSION_GROUP_URL = (right) => + `/api/pleroma/admin/users/permission_group/${right}` +const ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate' +const DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate' +const SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest' +const UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest' +const APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve' +const CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email' +const RESEND_CONFIRMATION_EMAIL_URL = + '/api/v1/pleroma/admin/users/resend_confirmation_email' +const LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) => + `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}` +const CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}` +const REQUIRE_PASSWORD_CHANGE_URL = + '/api/v1/pleroma/admin/users/force_password_reset' + +const DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa' +const EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' +const EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' +const EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` +const EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' +const EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip' +const EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) => + `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` +const EMOJI_UPDATE_FILE_URL = (name) => + `/api/v1/pleroma/emoji/packs/files?name=${name}` + +// + +export const setUsersTags = ({ + tags, + credentials, + value, + screen_names: nicknames, +}) => + promisedRequest({ + url: TAG_USER_URL, + method: value ? 'PUT' : 'DELETE', + credentials, + payload: { + nicknames, + tags, + }, + }) + +export const setUsersRight = ({ + right, + credentials, + value, + screen_names: nicknames, +}) => + promisedRequest({ + url: PERMISSION_GROUP_URL(right), + method: value ? 'POST' : 'DELETE', + credentials, + payload: { + nicknames, + }, + }) + +export const setUsersActivationStatus = ({ + credentials, + screen_names: nicknames, + value, +}) => + promisedRequest({ + url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersApprovalStatus = ({ + credentials, + screen_names: nicknames, +}) => + promisedRequest({ + url: APPROVE_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersConfirmationStatus = ({ + credentials, + screen_names: nicknames, +}) => + promisedRequest({ + url: CONFIRM_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersSuggestionStatus = ({ + credentials, + screen_names: nicknames, + value, +}) => + promisedRequest({ + url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const getUserData = ({ credentials, screen_name: nickname }) => + promisedRequest({ + url: USERS_URL(nickname), + method: 'GET', + credentials, + }) + +export const deleteAccounts = ({ credentials, screen_names: nicknames }) => + promisedRequest({ + url: USERS_URL(), + method: 'DELETE', + credentials, + payload: { + nicknames, + }, + }) + +export const getAnnouncements = ({ id, credentials }) => + promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials }) + +// the reported list is hardly useful because standards are for dating i guess, +// so make sure to fetchIfMissing right afterward using this call +export const listUsers = ({ opts, credentials }) => + promisedRequest({ + url: USERS_URL_LIST(opts), + credentials, + method: 'GET', + }) + +export const resendConfirmationEmail = ({ + screen_names: nicknames, + credentials, +}) => + promisedRequest({ + url: RESEND_CONFIRMATION_EMAIL_URL, + credentials, + method: 'PATCH', + payload: { + nicknames, + }, + }) + +export const requirePasswordChange = ({ + screen_names: nicknames, + credentials, +}) => + promisedRequest({ + url: REQUIRE_PASSWORD_CHANGE_URL, + credentials, + method: 'PATCH', + payload: { + nicknames, + }, + }) + +export const disableMFA = ({ screen_name: nickname, credentials }) => + promisedRequest({ + url: DISABLE_MFA_URL, + credentials, + method: 'PUT', + payload: { + nickname, + }, + }) + +export const listStatuses = ({ opts, credentials }) => + promisedRequest({ + url: LIST_STATUSES_URL(opts), + credentials, + method: 'GET', + }) + +export const changeStatusScope = ({ + opts: { id, sensitive, visibility }, + credentials, +}) => { + var payload = {} + if (typeof sensitive !== 'undefined') { + payload['sensitive'] = sensitive + } + if (typeof visibility !== 'undefined') { + payload['visibility'] = visibility + } + + return promisedRequest({ + url: CHANGE_STATUS_SCOPE_URL(id), + credentials, + method: 'PUT', + payload, + }) +} + +export const announcementToPayload = ({ + content, + startsAt, + endsAt, + allDay, +}) => { + const payload = { content } + + if (typeof startsAt !== 'undefined') { + payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null + } + + if (typeof endsAt !== 'undefined') { + payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null + } + + if (typeof allDay !== 'undefined') { + payload.all_day = allDay + } + + return payload +} + +export const postAnnouncement = ({ + credentials, + content, + startsAt, + endsAt, + allDay, +}) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(), + credentials, + method: 'POST', + payload: announcementToPayload({ content, startsAt, endsAt, allDay }), + }) + +export const editAnnouncement = ({ + id, + credentials, + content, + startsAt, + endsAt, + allDay, +}) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(id), + credentials, + method: 'PATCH', + payload: announcementToPayload({ content, startsAt, endsAt, allDay }), + }) + +export const deleteAnnouncement = ({ id, credentials }) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(id), + credentials, + method: 'DELETE', + }) + +export const setReportState = ({ id, state, credentials }) => { + return promisedRequest({ + url: REPORTS, + credentials, + method: 'PATCH', + payload: { + reports: [ + { + id, + state, + }, + ], + }, + }) +} + +export const getInstanceDBConfig = ({ credentials }) => + promisedRequest({ + url: CONFIG_URL, + credentials, + }) + +export const getInstanceConfigDescriptions = ({ credentials }) => + promisedRequest({ + url: DESCRIPTIONS_URL, + credentials, + }) + +export const getAvailableFrontends = ({ credentials }) => + promisedRequest({ + url: FRONTENDS_URL, + credentials, + }) + +export const pushInstanceDBConfig = ({ credentials, payload }) => + promisedRequest({ + url: CONFIG_URL, + method: 'POST', + credentials, + payload, + }) + +export const installFrontend = ({ credentials, payload }) => + promisedRequest({ + url: FRONTENDS_INSTALL_URL, + credentials, + method: 'POST', + payload, + }) + +// Emoji packs +export const deleteEmojiPack = ({ name }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + method: 'DELETE', + }) + +export const reloadEmoji = ({ credentials }) => + promisedRequest({ + url: EMOJI_RELOAD_URL, + method: 'POST', + credentials, + }) + +export const importEmojiFromFS = ({ credentials }) => + promisedRequest({ + url: EMOJI_IMPORT_FS_URL, + credentials, + }) + +export const createEmojiPack = ({ name, credentials }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + method: 'POST', + credentials, + }) + +export const listRemoteEmojiPacks = ({ + instance, + page, + pageSize, + credentials, +}) => { + if (!instance.startsWith('http')) { + instance = 'https://' + instance + } + + return promisedRequest({ + url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), + credentials, + }) +} + +export const downloadRemoteEmojiPack = ({ + instance, + packName, + as, + credentials, +}) => + promisedRequest({ + url: EMOJI_PACKS_DL_REMOTE_URL, + credentials, + method: 'POST', + payload: { + url: instance, + name: packName, + as, + }, + }) + +export const downloadRemoteEmojiPackZIP = ({ + url, + packName, + file, + credentials, +}) => { + const data = new FormData() + if (file) data.set('file', file) + if (url) data.set('url', url) + data.set('name', packName) + + return promisedRequest({ + url: EMOJI_PACKS_DL_REMOTE_ZIP_URL, + method: 'POST', + payload: data, + }) +} + +export const saveEmojiPackMetadata = ({ name, newData, credentials }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + credentials, + method: 'PATCH', + payload: { metadata: newData }, + }) + +export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { + const data = new FormData() + if (filename.trim() !== '') { + data.set('filename', filename) + } + if (shortcode.trim() !== '') { + data.set('shortcode', shortcode) + } + data.set('file', file) + + return promisedRequest({ + url: EMOJI_UPDATE_FILE_URL(packName), + method: 'POST', + payload: data, + }) +} + +export const updateEmojiFile = ({ + packName, + shortcode, + newShortcode, + newFilename, + credentials, + force, +}) => + promisedRequest({ + url: EMOJI_UPDATE_FILE_URL(packName), + credentials, + method: 'PATCH', + payload: { + shortcode, + new_shortcode: newShortcode, + new_filename: newFilename, + force, + }, + }) + +export const deleteEmojiFile = ({ packName, shortcode }) => + promisedRequest({ + url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, + method: 'DELETE', + }) diff --git a/src/api/chats.js b/src/api/chats.js new file mode 100644 index 000000000..51e83c74f --- /dev/null +++ b/src/api/chats.js @@ -0,0 +1,87 @@ +import { paramsString, promisedRequest } from './helpers.js' + +import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' +const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` +const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit }) => + `/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}` +const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` +const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => + `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` + +export const chats = ({ credentials }) => + promisedRequest({ + url: PLEROMA_CHATS_URL, + credentials, + }).then(({ data }) => ({ + chatList: data.map(parseChat).filter((c) => c), + })) + +export const getOrCreateChat = ({ accountId, credentials }) => + promisedRequest({ + url: PLEROMA_CHAT_URL(accountId), + method: 'POST', + credentials, + }) + +export const chatMessages = ({ + id, + credentials, + maxId, + sinceId, + limit = 20, +}) => { + return promisedRequest({ + url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }), + method: 'GET', + credentials, + }) +} + +export const sendChatMessage = ({ + id, + content, + mediaId = null, + idempotencyKey, + credentials, +}) => { + const payload = { + content, + } + + if (mediaId) { + payload.media_id = mediaId + } + + const headers = {} + + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + + return promisedRequest({ + url: PLEROMA_CHAT_MESSAGES_URL(id), + method: 'POST', + payload, + credentials, + headers, + }) +} + +export const readChat = ({ id, lastReadId, credentials }) => + promisedRequest({ + url: PLEROMA_CHAT_READ_URL(id), + method: 'POST', + payload: { + last_read_id: lastReadId, + }, + credentials, + }) + +export const deleteChatMessage = ({ chatId, messageId, credentials }) => + promisedRequest({ + url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), + method: 'DELETE', + credentials, + }) diff --git a/src/api/helpers.js b/src/api/helpers.js new file mode 100644 index 000000000..40b879232 --- /dev/null +++ b/src/api/helpers.js @@ -0,0 +1,135 @@ +import { snakeCase } from 'lodash' + +import { StatusCodeError } from 'src/services/errors/errors' + +export const paramsString = (params = {}) => { + if (params == null || params === undefined) return '' + + if (typeof params !== 'object' || Array.isArray(params)) { + throw new Error('Params are not an object!') + } + + const entries = (() => { + if (params instanceof Map) { + return params.entries() + } else { + return Object.entries(params) + } + })() + + if (entries.length === 0) return '' + + const arrays = [] + const nonArrays = [] + + entries.forEach(([k, v]) => { + if (v == null) return // Drop nulls + if ( + (typeof v === 'object' && !Array.isArray(v)) || + typeof v === 'function' + ) { + throw new Error('Param cannot be non-primitive!') + } + if (Array.isArray(v)) { + arrays.push([k, v]) + } else { + nonArrays.push([k, v]) + } + }) + + arrays.forEach(([k, array]) => { + array.forEach((v) => { + if ( + typeof v === 'object' || + typeof v === 'function' || + typeof v === 'undefined' + ) + throw new Error('Array param cannot contain non-primitives!') + }) + }) + + return ( + '?' + + [ + ...nonArrays.map(([k, v]) => [snakeCase(k), v]), + // turning [a,[1,2,3]] into [[a[],1],[a[],2],[a[],3]] + ...arrays.reduce( + (acc, [k, arrayValue]) => [ + ...acc, + ...arrayValue.map((v) => [snakeCase(k) + '[]', v]), + ], + [], + ), + ] + .map(([k, v]) => `${k}=${window.encodeURIComponent(v)}`) + .join('&') + ) +} + +export const promisedRequest = async ({ + method, + url, + payload, + formData, + credentials, + headers = {}, +}) => { + const options = { + method, + credentials: 'same-origin', + headers: { + Accept: 'application/json', + ...headers, + }, + } + + if (!formData) { + options.headers['Content-Type'] = 'application/json' + } + + if (formData || payload) { + options.body = formData || JSON.stringify(payload) + } + + if (credentials) { + options.headers = { + ...options.headers, + ...authHeaders(credentials), + } + } + + 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) + } +} + +const authHeaders = (accessToken) => { + if (accessToken) { + return { Authorization: `Bearer ${accessToken}` } + } else { + return {} + } +} diff --git a/src/api/mfa.js b/src/api/mfa.js new file mode 100644 index 000000000..8c1677fbe --- /dev/null +++ b/src/api/mfa.js @@ -0,0 +1,45 @@ +import { promisedRequest } from './helpers.js' + +export const verifyOTPCode = ({ + clientId, + clientSecret, + instance, + mfaToken, + code, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'totp') + + return promisedRequest({ + url: '/oauth/mfa/challenge', + method: 'POST', + formData, + }) +} + +export const verifyRecoveryCode = ({ + clientId, + clientSecret, + instance, + mfaToken, + code, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'recovery') + + return promisedRequest({ + url: `${instance}/oauth/mfa/challenge`, + method: 'POST', + formData, + }) +} diff --git a/src/api/oauth.js b/src/api/oauth.js new file mode 100644 index 000000000..f0fd2950d --- /dev/null +++ b/src/api/oauth.js @@ -0,0 +1,146 @@ +import { paramsString, promisedRequest } from './helpers.js' + +const REDIRECT_URI = `${window.location.origin}/oauth-callback` + +export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials' +export const MASTODON_APP_URL = '/api/v1/apps' +export const OAUTH_TOKEN_URL = '/oauth/token' +export const OAUTH_MFA_CHALLENGE_URL = '/oauth/mfa/challenge' +export const OAUTH_REVOKE_URL = '/oauth/revoke' + +export const createApp = () => { + const formData = new window.FormData() + + formData.append('client_name', 'PleromaFE') + formData.append('website', 'https://pleroma.social') + formData.append('redirect_uris', REDIRECT_URI) + formData.append('scopes', 'read write follow push admin') + + return promisedRequest({ + method: 'POST', + url: MASTODON_APP_URL, + formData, + }).then(({ data, ...rest }) => ({ + ...rest, + data: { + ...data, + clientId: data.client_id, + clientSecret: data.client_secret, + }, + })) +} + +export const verifyAppToken = ({ credentials }) => + promisedRequest({ + url: MASTODON_APP_VERIFY_URL, + credentials, + }) + +export const getLoginUrl = ({ instance, clientId }) => { + const data = { + responseType: 'code', + clientId, + redirectUri: REDIRECT_URI, + scope: 'read write follow push admin', + } + + return `${instance}/oauth/authorize${paramsString(data)}` +} + +export const getTokenWithCredentials = ({ + clientId, + clientSecret, + username, + password, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'password') + formData.append('username', username) + formData.append('password', password) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const getToken = ({ clientId, clientSecret, code }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'authorization_code') + formData.append('code', code) + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const getClientToken = ({ clientId, clientSecret }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'client_credentials') + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const verifyOTPCode = ({ app, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'totp') + + return promisedRequest({ + url: OAUTH_MFA_CHALLENGE_URL, + method: 'POST', + formData, + }) +} + +export const verifyRecoveryCode = ({ app, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'recovery') + + return promisedRequest({ + url: OAUTH_MFA_CHALLENGE_URL, + method: 'POST', + formData, + }) +} + +export const revokeToken = ({ app, token }) => { + const formData = new window.FormData() + + formData.append('client_id', app.clientId) + formData.append('client_secret', app.clientSecret) + formData.append('token', token) + + return promisedRequest({ + url: OAUTH_REVOKE_URL, + method: 'POST', + formData, + }) +} diff --git a/src/api/public.js b/src/api/public.js new file mode 100644 index 000000000..e001ba749 --- /dev/null +++ b/src/api/public.js @@ -0,0 +1,279 @@ +import { paramsString, promisedRequest } from './helpers.js' +import { MASTODON_USER_TIMELINE_URL } from './timelines.js' + +import { + parseSource, + parseStatus, + parseUser, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_SUGGESTIONS_URL = '/api/v1/suggestions' +const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' +const MASTODON_REGISTRATION_URL = '/api/v1/accounts' +const MASTODON_PASSWORD_RESET_URL = ({ email }) => + `/auth/password${paramsString({ email })}` + +const MASTODON_FOLLOWING_URL = ( + id, + { minId, maxId, sinceId, limit, withRelationships }, +) => + `/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` +const MASTODON_FOLLOWERS_URL = ( + id, + { minId, maxId, sinceId, limit, withRelationships }, +) => + `/api/v1/accounts/${id}/followers${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` + +export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` +const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` +const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` +const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` +const MASTODON_USER_URL = '/api/v1/accounts' +const MASTODON_USER_LOOKUP_URL = ({ acct }) => + `/api/v1/accounts/lookup${paramsString({ acct })}` +const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}` +const MASTODON_STATUS_FAVORITEDBY_URL = (id) => + `/api/v1/statuses/${id}/favourited_by` +const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => + `/api/v1/statuses/${id}/reblogged_by` +const MASTODON_SEARCH_2 = ({ + q, + resolve, + limit, + offset, + following, + type, + withRelationships, + accountId, + excludeUnreviewed, +}) => + `/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_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' +const PLEROMA_EMOJI_REACTIONS_URL = (id) => + `/api/v1/pleroma/statuses/${id}/reactions` +const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) => + `/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}` + +const EMOJI_PACKS_URL = (page, pageSize) => + `/api/v1/pleroma/emoji/packs${paramsString({ page, pageSize })}` + +// Params needed: +// nickname +// email +// fullname +// password +// password_confirm +// +// Optional +// bio +// homepage +// location +// token +// language +export const register = ({ params, credentials }) => { + const { nickname, ...rest } = params + return promisedRequest({ + url: MASTODON_REGISTRATION_URL, + method: 'POST', + credentials, + payload: { + nickname, + locale: 'en_US', + agreement: true, + ...rest, + }, + }) +} + +export const getCaptcha = () => + promisedRequest({ + url: '/api/pleroma/captcha', + }) + +export const fetchUser = ({ id, credentials }) => + promisedRequest({ + url: `${MASTODON_USER_URL}/${id}`, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const fetchUserByName = ({ name, credentials }) => + promisedRequest({ + url: MASTODON_USER_LOOKUP_URL({ acct: name }), + credentials, + }) + .then(({ data }) => data.id) + .catch((error) => { + if (error && error.statusCode === 404) { + // Either the backend does not support lookup endpoint, + // or there is no user with such name. Fallback and treat name as id. + return name + } else { + throw error + } + }) + .then((id) => fetchUser({ id, credentials })) + +export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => + promisedRequest({ + url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchFollowers = ({ + id, + maxId, + sinceId, + limit = 20, + credentials, +}) => + promisedRequest({ + url: MASTODON_FOLLOWERS_URL(id, { + maxId, + sinceId, + limit, + withRelationships: true, + }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_CONTEXT_URL(id), + credentials, + }).then((result) => ({ + ...result, + data: { + ...result.data, + ancestors: result.data.ancestors.map(parseStatus), + descendants: result.data.descendants.map(parseStatus), + }, + })) + +export const fetchStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const fetchStatusSource = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_SOURCE_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) })) + +export const fetchStatusHistory = ({ status, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_HISTORY_URL(status.id), + credentials, + }).then(({ data, ...rest }) => { + return [...data].reverse().map((item) => { + item.originalStatus = status + return { ...rest, data: parseStatus(item) } + }) + }) + +export const listEmojiPacks = ({ page, pageSize, credentials }) => + promisedRequest({ + url: EMOJI_PACKS_URL(page, pageSize), + }) + +export const fetchPinnedStatuses = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_USER_TIMELINE_URL(id, { pinned: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseStatus) })) + +export const verifyCredentials = ({ credentials }) => + promisedRequest({ + url: MASTODON_LOGIN_URL, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const resetPassword = ({ email }) => { + return promisedRequest({ + url: MASTODON_PASSWORD_RESET_URL({ email }), + method: 'POST', + }) +} + +export const suggestions = ({ credentials }) => + promisedRequest({ + url: MASTODON_SUGGESTIONS_URL, + credentials, + }) + +export const fetchPoll = ({ pollId, credentials }) => + promisedRequest({ + url: MASTODON_POLL_URL(encodeURIComponent(pollId)), + method: 'GET', + credentials, + }) + +export const fetchFavoritedByUsers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_FAVORITEDBY_URL(id), + method: 'GET', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchRebloggedByUsers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_REBLOGGEDBY_URL(id), + method: 'GET', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchEmojiReactions = ({ id, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_REACTIONS_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ + ...rest, + data: data.map((r) => { + r.accounts = r.accounts.map(parseUser) + return r + }), + })) + +export const searchUsers = ({ credentials, query }) => + promisedRequest({ + url: MASTODON_USER_SEARCH_URL({ q: query, resolve: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const search2 = ({ + credentials, + q, + resolve, + limit, + offset, + following, + type, +}) => { + return promisedRequest({ + url: MASTODON_SEARCH_2({ + q, + resolve, + limit, + offset, + following, + type, + withRelationships: true, + }), + credentials, + }).then(({ data, ...rest }) => { + data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u)) + data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s)) + return { ...rest, data } + }) +} + +export const fetchKnownDomains = ({ credentials }) => + promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) + +export const fetchScrobbles = ({ accountId, limit = 1 }) => + promisedRequest({ + url: PLEROMA_SCROBBLES_URL(accountId, { limit }), + }) diff --git a/src/api/timelines.js b/src/api/timelines.js new file mode 100644 index 000000000..0679b1eec --- /dev/null +++ b/src/api/timelines.js @@ -0,0 +1,198 @@ +import { paramsString, promisedRequest } from './helpers.js' + +import { + parseLinkHeaderPagination, + parseNotification, + parseStatus, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_USER_HOME_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_LIST_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_PUBLIC_TIMELINE = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, + local, + remote, + onlyMedia, +}) => + `/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}` +const MASTODON_TAG_TIMELINE_URL = ( + tag, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +export const MASTODON_USER_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia }, +) => + `/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}` +const MASTODON_USER_FAVORITES_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_BOOKMARK_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, + folderId, +}) => + `/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}` +const PLEROMA_STATUS_QUOTES_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const PLEROMA_USER_FAVORITES_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const AKKOMA_BUBBLE_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` + +const MASTODON_USER_NOTIFICATIONS_URL = ({ + minId, + sinceId, + maxId, + limit, + includeTypes, + replyVisibility, +}) => + `/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}` + +export const fetchTimeline = ({ + timeline, + credentials, + sinceId, + minId, + maxId, + userId, + listId, + statusId, + tag, + withMuted, + replyVisibility = 'all', + includeTypes = [], + bookmarkFolderId, +}) => { + const timelineUrls = { + friends: MASTODON_USER_HOME_TIMELINE_URL, + public: MASTODON_PUBLIC_TIMELINE, + publicAndExternal: MASTODON_PUBLIC_TIMELINE, + dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, + user: MASTODON_USER_TIMELINE_URL, + media: MASTODON_USER_TIMELINE_URL, + list: MASTODON_LIST_TIMELINE_URL, + favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, + publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, + bubble: AKKOMA_BUBBLE_TIMELINE_URL, + tag: MASTODON_TAG_TIMELINE_URL, + quotes: PLEROMA_STATUS_QUOTES_URL, + + notifications: MASTODON_USER_NOTIFICATIONS_URL, + } + const urlFunc = timelineUrls[timeline] + + const twoArgs = new Set([ + 'user', + 'media', + 'list', + 'publicFavorites', + 'tag', + 'quotes', + ]) + + const params = { + minId, + sinceId, + maxId, + limit: 20, + } + + const id = (() => { + switch (timeline) { + case 'user': + case 'media': + return userId + case 'list': + return listId + case 'quotes': + return statusId + case 'tag': + return tag + } + })() + + const isNotifications = timeline === 'notifications' + + if (timeline === 'media') { + params.onlyMedia = true + } + if (timeline === 'public') { + params.local = true + } + if (timeline !== 'favorites' && timeline !== 'bookmarks') { + params.withMuted = withMuted + } + if (replyVisibility !== 'all') { + params.replyVisibility = replyVisibility + } + if (timeline === 'bookmarks' && bookmarkFolderId) { + params.folderId = bookmarkFolderId + } + + if (isNotifications && includeTypes.length > 0) { + params.includeTypes = includeTypes + } + + const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params) + return promisedRequest({ url, credentials }).then((result) => { + const pagination = parseLinkHeaderPagination( + result.response.headers.get('Link'), + { + flakeId: timeline !== 'bookmarks' && timeline !== 'notifications', + }, + ) + + return { + ...result, + data: result.data.map(isNotifications ? parseNotification : parseStatus), + pagination, + } + }) +} diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 000000000..b9035da77 --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,924 @@ +import { concat, last } from 'lodash' + +import { paramsString, promisedRequest } from './helpers.js' +import { fetchFriends, MASTODON_STATUS_URL } from './public.js' + +import { + parseAttachment, + parseStatus, + parseUser, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' +const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' +const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' +const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' +const CHANGE_EMAIL_URL = '/api/pleroma/change_email' +const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' +const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' +const ALIASES_URL = '/api/pleroma/aliases' +const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' +const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' + +const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' +const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' + +const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' +const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' +const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' + +const MASTODON_DISMISS_NOTIFICATION_URL = (id) => + `/api/v1/notifications/${id}/dismiss` +const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` +const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` +const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` +const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` +const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` +const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` +const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` + +const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' +const MASTODON_APPROVE_USER_URL = (id) => + `/api/v1/follow_requests/${id}/authorize` +const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` +const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) => + `/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}` +const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` +export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}` +export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` +const MASTODON_USER_BLOCKS_URL = ({ + maxId, + sinceId, + limit, + withRelationships, +}) => + `/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}` +const MASTODON_USER_MUTES_URL = ({ + maxId, + sinceId, + limit, + withRelationships, +}) => + `/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}` +const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` +const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` +const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` +const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` +const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => + `/api/v1/accounts/${id}/remove_from_followers` +const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` +const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` +const MASTODON_UNBOOKMARK_STATUS_URL = (id) => + `/api/v1/statuses/${id}/unbookmark` +const MASTODON_POST_STATUS_URL = '/api/v1/statuses' +const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' +const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` +const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' +const MASTODON_REPORT_USER_URL = '/api/v1/reports' +const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` +const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` +const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` +const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` +const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' +const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' +const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => + `/api/v1/announcements/${id}/dismiss` +const PLEROMA_EMOJI_REACT_URL = (id, emoji) => + `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => + `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' +const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' +const PLEROMA_BOOKMARK_FOLDER_URL = (id) => + `/api/v1/pleroma/bookmark_folders/${id}` + +// #Posts +export const favorite = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_FAVORITE_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unfavorite = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNFAVORITE_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const retweet = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_RETWEET_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unretweet = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNRETWEET_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const reactWithEmoji = ({ id, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_REACT_URL(id, emoji), + method: 'PUT', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unreactWithEmoji = ({ id, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), + method: 'DELETE', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const bookmarkStatus = ({ id, credentials, ...options }) => + promisedRequest({ + url: MASTODON_BOOKMARK_STATUS_URL(id), + credentials, + method: 'POST', + payload: { + folder_id: options.folder_id, + }, + }) + +export const unbookmarkStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNBOOKMARK_STATUS_URL(id), + credentials, + method: 'POST', + }) + +export const pinOwnStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_PIN_OWN_STATUS(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unpinOwnStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNPIN_OWN_STATUS(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const muteConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_MUTE_CONVERSATION(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unmuteConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNMUTE_CONVERSATION(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const vote = ({ pollId, choices, credentials }) => { + const form = new FormData() + form.append('choices', choices) + + return promisedRequest({ + url: MASTODON_VOTE_URL(encodeURIComponent(pollId)), + method: 'POST', + credentials, + payload: { + choices, + }, + }) +} + +// #Posting +export const postStatus = ({ + credentials, + status, + spoilerText, + visibility, + sensitive, + poll, + mediaIds = [], + inReplyToStatusId, + quoteId, + contentType, + preview, + idempotencyKey, +}) => { + const form = new FormData() + const pollOptions = poll.options || [] + + form.append('status', status) + form.append('source', 'Pleroma FE') + if (spoilerText) form.append('spoiler_text', spoilerText) + if (visibility) form.append('visibility', visibility) + if (sensitive) form.append('sensitive', sensitive) + if (contentType) form.append('content_type', contentType) + mediaIds.forEach((val) => { + form.append('media_ids[]', val) + }) + if (pollOptions.some((option) => option !== '')) { + const normalizedPoll = { + expires_in: parseInt(poll.expiresIn, 10), + multiple: poll.multiple, + } + Object.keys(normalizedPoll).forEach((key) => { + form.append(`poll[${key}]`, normalizedPoll[key]) + }) + + pollOptions.forEach((option) => { + form.append('poll[options][]', option) + }) + } + if (inReplyToStatusId) { + form.append('in_reply_to_id', inReplyToStatusId) + } + if (quoteId) { + form.append('quote_id', quoteId) + } + if (preview) { + form.append('preview', 'true') + } + + const headers = {} + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + + return promisedRequest({ + url: MASTODON_POST_STATUS_URL, + formData: form, + method: 'POST', + credentials, + headers, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) +} + +export const editStatus = ({ + id, + credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds = [], + contentType, +}) => { + const form = new FormData() + const pollOptions = poll.options || [] + + form.append('status', status) + if (spoilerText) form.append('spoiler_text', spoilerText) + if (sensitive) form.append('sensitive', sensitive) + if (contentType) form.append('content_type', contentType) + mediaIds.forEach((val) => { + form.append('media_ids[]', val) + }) + + if (pollOptions.some((option) => option !== '')) { + const normalizedPoll = { + expires_in: parseInt(poll.expiresIn, 10), + multiple: poll.multiple, + } + Object.keys(normalizedPoll).forEach((key) => { + form.append(`poll[${key}]`, normalizedPoll[key]) + }) + + pollOptions.forEach((option) => { + form.append('poll[options][]', option) + }) + } + + return promisedRequest({ + url: MASTODON_STATUS_URL(id), + formData: form, + method: 'PUT', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) +} + +export const deleteStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_DELETE_URL(id), + credentials, + method: 'DELETE', + }) + +export const uploadMedia = ({ formData, credentials }) => + promisedRequest({ + url: MASTODON_MEDIA_UPLOAD_URL, + formData, + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) + +export const setMediaDescription = ({ id, description, credentials }) => + promisedRequest({ + url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, + method: 'PUT', + credentials, + payload: { + description, + }, + }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) + +// #Notifications +export const dismissNotification = ({ credentials, id }) => + promisedRequest({ + url: MASTODON_DISMISS_NOTIFICATION_URL(id), + method: 'POST', + payload: { id }, + credentials, + }) + +export const markNotificationsAsSeen = ({ + id, + credentials, + single = false, +}) => { + const formData = new FormData() + + if (single) { + formData.append('id', id) + } else { + formData.append('max_id', id) + } + + return promisedRequest({ + url: NOTIFICATION_READ_URL, + formData, + credentials, + method: 'POST', + }) +} + +// #Announcements +export const getAnnouncements = ({ credentials }) => + promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials }) + +export const dismissAnnouncement = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id), + credentials, + method: 'POST', + }) + +// #Imports +export const importMutes = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: MUTES_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const importBlocks = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: BLOCKS_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const importFollows = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: FOLLOW_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const exportFriends = ({ id, credentials }) => { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this + return new Promise(async (resolve, reject) => { + try { + let friends = [] + let more = true + while (more) { + const maxId = friends.length > 0 ? last(friends).id : undefined + const users = await fetchFriends({ + id, + maxId, + credentials, + withRelationships: true, + }) + friends = concat(friends, users) + if (users.length === 0) { + more = false + } + } + resolve(friends) + } catch (err) { + reject(err) + } + }) +} + +// #Profile settings +export const updateNotificationSettings = ({ credentials, settings }) => { + return promisedRequest({ + url: NOTIFICATION_SETTINGS_URL, + credentials, + method: 'PUT', + payload: settings, + }) +} + +export const updateProfileImages = ({ + credentials, + avatar = null, + avatarName = null, + banner = null, + background = null, +}) => { + const form = new FormData() + if (avatar !== null) { + if (avatarName !== null) { + form.append('avatar', avatar, avatarName) + } else { + form.append('avatar', avatar) + } + } + if (banner !== null) form.append('header', banner) + if (background !== null) form.append('pleroma_background_image', background) + return promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + method: 'PATCH', + formData: form, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) +} + +export const updateProfile = ({ credentials, params }) => { + const formData = new FormData() + + for (const name in params) { + if (name === 'fields_attributes') { + params[name].forEach((param, i) => { + formData.append(name + `[${i}][name]`, param.name) + formData.append(name + `[${i}][value]`, param.value) + }) + } else { + if (typeof params[name] === 'object') { + console.warn( + 'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.', + ) + console.warn('Object:\n' + JSON.stringify(params[name], null, 2)) + } + formData.append(name, params[name]) + } + } + + return promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + method: 'PATCH', + formData, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) +} + +export const updateProfileJSON = ({ credentials, params }) => + promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + payload: params, + method: 'PATCH', + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const changeEmail = ({ credentials, email, password }) => { + const form = new FormData() + + form.append('email', email) + form.append('password', password) + + return promisedRequest({ + url: CHANGE_EMAIL_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +export const moveAccount = ({ credentials, password, targetAccount }) => { + const form = new FormData() + + form.append('password', password) + form.append('target_account', targetAccount) + + return promisedRequest({ + url: MOVE_ACCOUNT_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +export const changePassword = ({ + credentials, + password, + newPassword, + newPasswordConfirmation, +}) => { + const form = new FormData() + + form.append('password', password) + form.append('new_password', newPassword) + form.append('new_password_confirmation', newPasswordConfirmation) + + return promisedRequest({ + url: CHANGE_PASSWORD_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +// #MFA +export const settingsMFA = ({ credentials }) => + promisedRequest({ + url: MFA_SETTINGS_URL, + credentials, + method: 'GET', + }) + +export const mfaDisableOTP = ({ credentials, password }) => { + const form = new FormData() + + form.append('password', password) + + return promisedRequest({ + url: MFA_DISABLE_OTP_URL, + formData: form, + method: 'DELETE', + credentials, + }) +} + +export const mfaConfirmOTP = ({ credentials, password, token }) => { + const form = new FormData() + + form.append('password', password) + form.append('code', token) + + return promisedRequest({ + url: MFA_CONFIRM_OTP_URL, + formData: form, + credentials, + method: 'POST', + }) +} +export const mfaSetupOTP = ({ credentials }) => + promisedRequest({ + url: MFA_SETUP_OTP_URL, + credentials, + method: 'GET', + }) +export const generateMfaBackupCodes = ({ credentials }) => + promisedRequest({ + url: MFA_BACKUP_CODES_URL, + credentials, + method: 'GET', + }) + +// #Aliases +export const addAlias = ({ credentials, alias }) => + promisedRequest({ + url: ALIASES_URL, + method: 'PUT', + credentials, + payload: { alias }, + }) + +export const deleteAlias = ({ credentials, alias }) => + promisedRequest({ + url: ALIASES_URL, + method: 'DELETE', + credentials, + payload: { alias }, + }) + +export const listAliases = ({ credentials }) => + promisedRequest({ + url: ALIASES_URL, + method: 'GET', + credentials, + params: { + _cacheBooster: new Date().getTime(), + }, + }) + +// User manipulation +export const fetchUserRelationship = ({ id, withSuspended, credentials }) => + promisedRequest({ + url: MASTODON_USER_RELATIONSHIPS_URL({ id, withSuspended }), + credentials, + }) + +export const followUser = ({ id, credentials, ...options }) => { + const payload = {} + + if (options.reblogs !== undefined) { + payload.reblogs = options.reblogs + } + + if (options.notify !== undefined) { + payload.notify = options.notify + } + + return promisedRequest({ + url: MASTODON_FOLLOW_URL(id), + payload, + credentials, + method: 'POST', + }) +} + +export const unfollowUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNFOLLOW_URL(id), + credentials, + method: 'POST', + }) +export const fetchUserInLists = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_USER_IN_LISTS(id), + credentials, + }) + +export const removeUserFromFollowers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), + credentials, + method: 'POST', + }) + +export const fetchFollowRequests = ({ credentials }) => + promisedRequest({ + url: MASTODON_FOLLOW_REQUESTS_URL, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const approveUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_APPROVE_USER_URL(id), + credentials, + method: 'POST', + }) + +export const denyUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_DENY_USER_URL(id), + credentials, + method: 'POST', + }) + +export const editUserNote = ({ id, credentials, comment }) => + promisedRequest({ + url: MASTODON_USER_NOTE_URL(id), + credentials, + payload: { + comment, + }, + method: 'POST', + }) + +export const fetchMutes = ({ maxId, credentials }) => + promisedRequest({ + url: MASTODON_USER_MUTES_URL({ maxId, withRelationships: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const muteUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.expires_in = expiresIn + } + + return promisedRequest({ + url: MASTODON_MUTE_USER_URL(id), + credentials, + method: 'POST', + payload, + }) +} + +export const unmuteUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNMUTE_USER_URL(id), + credentials, + method: 'POST', + }) + +export const fetchBlocks = ({ maxId, credentials }) => + promisedRequest({ + url: MASTODON_USER_BLOCKS_URL({ maxId, withRelationships: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const blockUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.duration = expiresIn + } + + return promisedRequest({ + url: MASTODON_BLOCK_USER_URL(id), + credentials, + method: 'POST', + payload, + }) +} + +export const unblockUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNBLOCK_USER_URL(id), + credentials, + method: 'POST', + }) + +export const reportUser = ({ + credentials, + userId, + statusIds, + comment, + forward, +}) => + promisedRequest({ + url: MASTODON_REPORT_USER_URL, + method: 'POST', + payload: { + account_id: userId, + status_ids: statusIds, + comment, + forward, + }, + credentials, + }) + +// #Domain mutes +export const fetchDomainMutes = ({ credentials }) => + promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) + +export const muteDomain = ({ domain, credentials }) => + promisedRequest({ + url: MASTODON_DOMAIN_BLOCKS_URL, + method: 'POST', + payload: { domain }, + credentials, + }) + +export const unmuteDomain = ({ domain, credentials }) => + promisedRequest({ + url: MASTODON_DOMAIN_BLOCKS_URL, + method: 'DELETE', + payload: { domain }, + credentials, + }) + +// #Backups +export const addBackup = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BACKUP_URL, + method: 'POST', + credentials, + }) + +export const listBackups = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BACKUP_URL, + method: 'GET', + credentials, + params: { + _cacheBooster: new Date().getTime(), + }, + }) + +// #OAuth +export const fetchOAuthTokens = ({ credentials }) => + promisedRequest({ + url: '/api/oauth_tokens.json', + credentials, + }) + +export const revokeOAuthToken = ({ id, credentials }) => + promisedRequest({ + url: `/api/oauth_tokens/${id}`, + credentials, + method: 'DELETE', + }) + +// #Lists +export const fetchLists = ({ credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(), + credentials, + }) + +export const createList = ({ title, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(), + credentials, + method: 'POST', + payload: { title }, + }) + +export const getList = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + credentials, + }) + +export const updateList = ({ listId, title, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + + credentials, + method: 'PUT', + payload: { title }, + }) + +export const getListAccounts = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + }).then((data) => data.map(({ id }) => id)) + +export const addAccountsToList = ({ listId, accountIds, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + method: 'POST', + payload: { account_ids: accountIds }, + }) + +export const removeAccountsFromList = ({ listId, accountIds, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + method: 'DELETE', + payload: { account_ids: accountIds }, + }) + +export const deleteList = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + method: 'DELETE', + credentials, + }) + +// #Bookmarks +export const fetchBookmarkFolders = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDERS_URL, + credentials, + }) + +export const createBookmarkFolder = ({ name, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDERS_URL, + credentials, + method: 'POST', + payload: { name, emoji }, + }) + +export const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), + credentials, + method: 'PATCH', + payload: { name, emoji }, + }) + +export const deleteBookmarkFolder = ({ folderId, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), + method: 'DELETE', + credentials, + }) + +// #So long and thanks for all the fish +export const deleteAccount = ({ credentials, password }) => { + const formData = new FormData() + + formData.append('password', password) + + return promisedRequest({ + url: DELETE_ACCOUNT_URL, + formData, + method: 'POST', + credentials, + }) +} diff --git a/src/api/websocket.js b/src/api/websocket.js new file mode 100644 index 000000000..d952372e5 --- /dev/null +++ b/src/api/websocket.js @@ -0,0 +1,176 @@ +import { paramsString } 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, +}) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index fe65a7387..0b4825ca0 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -19,7 +19,6 @@ import { config.autoAddCss = false import App from '../App.vue' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { applyStyleConfig } from '../services/style_setter/style_setter.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -29,7 +28,6 @@ import { } from '../services/window_utils/window_utils' import routes from './routes' -import { useAnnouncementsStore } from 'src/stores/announcements' import { useAuthFlowStore } from 'src/stores/auth_flow' import { useEmojiStore } from 'src/stores/emoji.js' import { useI18nStore } from 'src/stores/i18n' @@ -38,7 +36,7 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j import { useInterfaceStore } from 'src/stores/interface.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useOAuthStore } from 'src/stores/oauth' +import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' @@ -261,16 +259,6 @@ const getStickers = async ({ store }) => { } } -const getAppSecret = async ({ store }) => { - const oauth = useOAuthStore() - if (oauth.userToken) { - store.commit( - 'setBackendInteractor', - backendInteractorService(oauth.getToken), - ) - } -} - const resolveStaffAccounts = ({ store, accounts }) => { const nicknames = accounts.map((uri) => uri.split('/').pop()) useInstanceStore().set({ @@ -461,14 +449,13 @@ const setConfig = async ({ store }) => { const apiConfig = configInfos[0] const staticConfig = configInfos[1] - getAppSecret({ store }) await setSettings({ store, apiConfig, staticConfig }) } const checkOAuthToken = async ({ store }) => { const oauth = useOAuthStore() - if (oauth.getUserToken) { - return store.dispatch('loginUser', oauth.getUserToken) + if (oauth.userToken) { + return store.dispatch('loginUser', oauth.userToken) } return Promise.resolve() } @@ -578,10 +565,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { getInstanceConfig({ store }), ]).catch((e) => Promise.reject(e)) - // Start fetching things that don't need to block the UI - store.dispatch('fetchMutes') - store.dispatch('loadDrafts') - useAnnouncementsStore().startFetchingAnnouncements() getTOS({ store }) getStickers({ store }) diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js index 385a6f796..0f7933e5f 100644 --- a/src/components/announcements_page/announcements_page.js +++ b/src/components/announcements_page/announcements_page.js @@ -35,9 +35,7 @@ const AnnouncementsPage = { canPostAnnouncement() { return ( this.currentUser && - this.currentUser.privileges.has( - 'announcements_manage_announcements', - ) + this.currentUser.privileges.has('announcements_manage_announcements') ) }, }, diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 4b733258f..d5bce12a7 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -2,7 +2,6 @@ import { mapState } from 'pinia' import { defineAsyncComponent } from 'vue' import Popover from 'src/components/popover/popover.vue' -import VideoAttachment from 'src/components/video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import { useInstanceStore } from 'src/stores/instance.js' diff --git a/src/components/bookmark_folder_edit/bookmark_folder_edit.js b/src/components/bookmark_folder_edit/bookmark_folder_edit.js index 43aa239b2..ae5aebedd 100644 --- a/src/components/bookmark_folder_edit/bookmark_folder_edit.js +++ b/src/components/bookmark_folder_edit/bookmark_folder_edit.js @@ -1,8 +1,10 @@ import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue' -import apiService from '../../services/api/api.service' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchBookmarkFolders } from 'src/api/user.js' const BookmarkFolderEdit = { data() { @@ -22,8 +24,10 @@ const BookmarkFolderEdit = { }, created() { if (!this.id) return - const credentials = this.$store.state.users.currentUser.credentials - apiService.fetchBookmarkFolders({ credentials }).then((folders) => { + + fetchBookmarkFolders({ + credentials: useOAuthStore().token, + }).then(({ data: folders }) => { const folder = folders.find((folder) => folder.id === this.id) if (!folder) return diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 7428c0dc7..16a03ab1d 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -5,7 +5,6 @@ import { mapGetters, mapState } from 'vuex' import ChatMessage from 'src/components/chat_message/chat_message.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' -import { WSConnectionStatus } from '../../services/api/api.service.js' import chatService from '../../services/chat_service/chat_service.js' import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js' @@ -17,6 +16,14 @@ import { } from './chat_layout_utils.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + chatMessages, + getOrCreateChat, + sendChatMessage, +} from 'src/api/chats.js' +import { WSConnectionStatus } from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' @@ -115,7 +122,6 @@ const Chat = { mobileLayout: (store) => store.layoutType === 'mobile', }), ...mapState({ - backendInteractor: (state) => state.api.backendInteractor, mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, currentUser: (state) => state.users.currentUser, }), @@ -267,43 +273,48 @@ const Chat = { const fetchOlderMessages = !!maxId const sinceId = fetchLatest && chatMessageService.maxId - return this.backendInteractor - .chatMessages({ id: chatId, maxId, sinceId }) - .then((messages) => { - // Clear the current chat in case we're recovering from a ws connection loss. - if (isFirstFetch) { - chatService.clear(chatMessageService) - } + return chatMessages({ + id: chatId, + maxId, + sinceId, + credentials: useOAuthStore().token, + }).then(({ data: messages }) => { + // Clear the current chat in case we're recovering from a ws connection loss. + if (isFirstFetch) { + chatService.clear(chatMessageService) + } - const positionBeforeUpdate = getScrollPosition() - this.$store - .dispatch('addChatMessages', { chatId, messages }) - .then(() => { - this.$nextTick(() => { - if (fetchOlderMessages) { - this.handleScrollUp(positionBeforeUpdate) - } + const positionBeforeUpdate = getScrollPosition() + this.$store + .dispatch('addChatMessages', { chatId, messages }) + .then(() => { + this.$nextTick(() => { + if (fetchOlderMessages) { + this.handleScrollUp(positionBeforeUpdate) + } - // In vertical screens, the first batch of fetched messages may not always take the - // full height of the scrollable container. - // If this is the case, we want to fetch the messages until the scrollable container - // is fully populated so that the user has the ability to scroll up and load the history. - if (!isScrollable() && messages.length > 0) { - this.fetchChat({ - maxId: this.currentChatMessageService.minId, - }) - } - }) + // In vertical screens, the first batch of fetched messages may not always take the + // full height of the scrollable container. + // If this is the case, we want to fetch the messages until the scrollable container + // is fully populated so that the user has the ability to scroll up and load the history. + if (!isScrollable() && messages.length > 0) { + this.fetchChat({ + maxId: this.currentChatMessageService.minId, + }) + } }) - }) + }) + }) }, async startFetching() { let chat = this.findOpenedChatByRecipientId(this.recipientId) if (!chat) { try { - chat = await this.backendInteractor.getOrCreateChat({ + const { data } = await getOrCreateChat({ accountId: this.recipientId, + credentials: useOAuthStore().token, }) + chat = data } catch (e) { console.error('Error creating or getting a chat', e) this.errorLoadingChat = true @@ -369,9 +380,11 @@ const Chat = { doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { if (retriesLeft <= 0) return - this.backendInteractor - .sendChatMessage(params) - .then((data) => { + sendChatMessage({ + params, + credentials: useOAuthStore().token, + }) + .then(({ data }) => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, updateMaxId: false, diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index ba06172ed..2066a8194 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -1,6 +1,5 @@ import { mapState as mapPiniaState } from 'pinia' -import { defineAsyncComponent } from 'vue' -import { mapGetters, mapState } from 'vuex' +import { mapState } from 'vuex' import Attachment from 'src/components/attachment/attachment.vue' import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue' diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js index 5ee2b610d..0ee4e292b 100644 --- a/src/components/chat_new/chat_new.js +++ b/src/components/chat_new/chat_new.js @@ -3,6 +3,8 @@ import { mapGetters, mapState } from 'vuex' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue' +import { useOAuthStore } from 'src/stores/oauth.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons' @@ -22,7 +24,9 @@ const chatNew = { } }, async created() { - const { chats } = await this.backendInteractor.chats() + const { chats } = await chats({ + credentials: useOAuthStore().token, + }) chats.forEach((chat) => this.suggestions.push(chat.account)) }, computed: { @@ -38,7 +42,6 @@ const chatNew = { }, ...mapState({ currentUser: (state) => state.users.currentUser, - backendInteractor: (state) => state.api.backendInteractor, }), ...mapGetters(['findUser']), }, diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js index 52d4445b6..1d12384cc 100644 --- a/src/components/chat_title/chat_title.js +++ b/src/components/chat_title/chat_title.js @@ -1,5 +1,3 @@ -import { defineAsyncComponent } from 'vue' - import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserPopover from 'src/components/user_popover/user_popover.vue' diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index da15c79d6..54bcbd373 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -5,10 +5,13 @@ import { mapState } from 'vuex' import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue' import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue' import ThreadTree from 'src/components/thread_tree/thread_tree.vue' -import { WSConnectionStatus } from '../../services/api/api.service.js' 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 { library } from '@fortawesome/fontawesome-svg-core' import { @@ -436,22 +439,26 @@ const conversation = { methods: { fetchConversation() { if (this.status) { - this.$store.state.api.backendInteractor - .fetchConversation({ id: this.statusId }) - .then(({ ancestors, descendants }) => { - this.$store.dispatch('addNewStatuses', { statuses: ancestors }) - this.$store.dispatch('addNewStatuses', { statuses: descendants }) - this.setHighlight(this.originalStatusId) - }) + fetchConversation({ + id: this.statusId, + credentials: useOAuthStore().token, + }).then(({ data: { ancestors, descendants } }) => { + this.$store.dispatch('addNewStatuses', { statuses: ancestors }) + this.$store.dispatch('addNewStatuses', { statuses: descendants }) + this.setHighlight(this.originalStatusId) + }) } else { this.loadStatusError = null - this.$store.state.api.backendInteractor - .fetchStatus({ id: this.statusId }) - .then((status) => { + fetchStatus({ + id: this.statusId, + credentials: useOAuthStore().token, + }) + .then(({ data: status }) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() }) .catch((error) => { + console.error(error) this.loadStatusError = error }) } diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index c9a6abcf1..fe8e9e105 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -1,5 +1,3 @@ -import { useEmojiStore } from 'src/stores/emoji.js' - /** * suggest - generates a suggestor function to be used by emoji-input * data: object providing source information for specific types of suggestions: diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index bc2bb092b..6889fb8b0 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -6,7 +6,6 @@ import Popover from 'src/components/popover/popover.vue' import { ensureFinalFallback } from '../../i18n/languages.js' import { useEmojiStore } from 'src/stores/emoji.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { library } from '@fortawesome/fontawesome-svg-core' diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index 44658e985..c2e10c242 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -4,6 +4,9 @@ import { notificationsFromStore } from '../../services/notification_utils/notifi import BasicUserCard from '../basic_user_card/basic_user_card.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { approveUser, denyUser } from 'src/api/user.js' const FollowRequestCard = { props: ['user'], @@ -48,7 +51,10 @@ const FollowRequestCard = { } }, doApprove() { - this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) + approveUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }) this.$store.dispatch('removeFollowRequest', this.user) const notifId = this.findFollowRequestNotificationId() @@ -70,12 +76,14 @@ const FollowRequestCard = { }, doDeny() { const notifId = this.findFollowRequestNotificationId() - this.$store.state.api.backendInteractor - .denyUser({ id: this.user.id }) - .then(() => { - this.$store.dispatch('dismissNotificationLocal', { id: notifId }) - this.$store.dispatch('removeFollowRequest', this.user) - }) + + denyUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }).then(() => { + this.$store.dispatch('dismissNotificationLocal', { id: notifId }) + this.$store.dispatch('removeFollowRequest', this.user) + }) this.hideDenyConfirmDialog() }, }, diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js index d96f14599..337ee4d4f 100644 --- a/src/components/lists_menu/lists_menu_content.js +++ b/src/components/lists_menu/lists_menu_content.js @@ -4,7 +4,6 @@ import { mapState } from 'vuex' import { getListEntries } from 'src/components/navigation/filter.js' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' -import { useInstanceStore } from 'src/stores/instance.js' import { useListsStore } from 'src/stores/lists.js' export const ListsMenuContent = { diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index b16fb8d2f..a333e6970 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,12 +1,12 @@ -import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia' +import { mapActions, mapState as mapPiniaState } from 'pinia' import { mapState } from 'vuex' -import oauthApi from '../../services/new_api/oauth.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -35,19 +35,13 @@ const LoginForm = { this.isTokenAuth ? this.submitToken() : this.submitPassword() }, submitToken() { - const data = { - instance: this.server, - commit: this.$store.commit, - } - // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. this.ensureAppToken().then(() => { - const app = { + window.location.href = getLoginUrl({ clientId: this.clientId, - clientSecret: this.clientSecret, - } - oauthApi.login({ ...app, ...data }) + instance: this.server, + }) }) }, submitPassword() { @@ -56,37 +50,32 @@ const LoginForm = { // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. this.ensureAppToken().then(() => { - const app = { + getTokenWithCredentials({ clientId: this.clientId, clientSecret: this.clientSecret, - } - - oauthApi - .getTokenWithCredentials({ - ...app, - instance: this.server, - username: this.user.username, - password: this.user.password, - }) - .then((result) => { - if (result.error) { - if (result.error === 'mfa_required') { - this.requireMFA({ settings: result }) - } else if (result.identifier === 'password_reset_required') { - this.$router.push({ - name: 'password-reset', - params: { passwordResetRequested: true }, - }) - } else { - this.error = result.error - this.focusOnPasswordInput() - } - return - } + instance: this.server, + username: this.user.username, + password: this.user.password, + }) + .then(({ data: result }) => { this.login(result).then(() => { this.$router.push({ name: 'friends' }) }) }) + .catch((error) => { + if (error.errorData?.error === 'mfa_required') { + this.requireMFA({ settings: error }) + } else if (error.identifier === 'password_reset_required') { + this.$router.push({ + name: 'password-reset', + params: { passwordResetRequested: true }, + }) + } else { + this.error = error + this.focusOnPasswordInput() + } + return + }) }) }, clearError() { diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index ad53b6e31..fe5994f7c 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,7 +1,6 @@ import { defineAsyncComponent } from 'vue' import Modal from 'src/components/modal/modal.vue' -import StillImage from 'src/components/still-image/still-image.vue' import GestureService from '../../services/gesture_service/gesture_service' import { useMediaViewerStore } from 'src/stores/media_viewer.js' diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 877e83c12..48f6d1d8e 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -1,6 +1,5 @@ import { mapState as mapPiniaState } from 'pinia' -import { defineAsyncComponent } from 'vue' -import { mapGetters, mapState } from 'vuex' +import { mapState } from 'vuex' import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue' diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js index f4ddcadd0..e04218bd8 100644 --- a/src/components/mfa_form/recovery_form.js +++ b/src/components/mfa_form/recovery_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import mfaApi from '../../services/new_api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyRecoveryCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -43,18 +43,18 @@ export default { code: this.code, } - mfaApi.verifyRecoveryCode(data).then((result) => { - if (result.error) { - this.error = result.error + verifyRecoveryCode(data) + .then((result) => { + this.login(result).then(() => { + this.$router.push({ name: 'friends' }) + }) + }) + .catch((error) => { + this.error = error this.code = null this.focusOnCodeInput() return - } - - this.login(result).then(() => { - this.$router.push({ name: 'friends' }) }) - }) }, }, } diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js index 6d51cba94..056098c25 100644 --- a/src/components/mfa_form/totp_form.js +++ b/src/components/mfa_form/totp_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import mfaApi from '../../services/new_api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyOTPCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -46,18 +46,18 @@ export default { code: this.code, } - mfaApi.verifyOTPCode(data).then((result) => { - if (result.error) { - this.error = result.error + verifyOTPCode(data) + .then(({ data: result }) => { + this.login(result).then(() => { + this.$router.push({ name: 'friends' }) + }) + }) + .catch((error) => { + this.error = error this.code = null this.focusOnCodeInput() return - } - - this.login(result).then(() => { - this.$router.push({ name: 'friends' }) }) - }) }, }, } diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index b819c72aa..ca4852ab3 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -4,7 +4,6 @@ import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import Popover from 'src/components/popover/popover.vue' import { useAdminSettingsStore } from 'src/stores/admin_settings.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { library } from '@fortawesome/fontawesome-svg-core' diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 856850920..b6ee27f9c 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -15,8 +15,10 @@ import { import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { approveUser, denyUser } from 'src/api/user.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { library } from '@fortawesome/fontawesome-svg-core' @@ -142,7 +144,10 @@ const Notification = { } }, doApprove() { - this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) + approveUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }) this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id, @@ -163,14 +168,15 @@ const Notification = { } }, doDeny() { - this.$store.state.api.backendInteractor - .denyUser({ id: this.user.id }) - .then(() => { - this.$store.dispatch('dismissNotificationLocal', { - id: this.notification.id, - }) - this.$store.dispatch('removeFollowRequest', this.user) + denyUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }).then(() => { + this.$store.dispatch('dismissNotificationLocal', { + id: this.notification.id, }) + this.$store.dispatch('removeFollowRequest', this.user) + }) this.hideDenyConfirmDialog() }, }, diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js index 4bcb9803e..04f79425e 100644 --- a/src/components/oauth_callback/oauth_callback.js +++ b/src/components/oauth_callback/oauth_callback.js @@ -1,8 +1,8 @@ -import oauth from '../../services/new_api/oauth.js' - import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { getToken } from 'src/api/oauth.js' + const oac = { props: ['code'], mounted() { @@ -10,18 +10,16 @@ const oac = { const oauthStore = useOAuthStore() const { clientId, clientSecret } = oauthStore - oauth - .getToken({ - clientId, - clientSecret, - instance: useInstanceStore().server, - code: this.code, - }) - .then((result) => { - oauthStore.setToken(result.access_token) - this.$store.dispatch('loginUser', result.access_token) - this.$router.push({ name: 'friends' }) - }) + getToken({ + clientId, + clientSecret, + instance: useInstanceStore().server, + code: this.code, + }).then(({ data: result }) => { + oauthStore.setToken(result.access_token) + this.$store.dispatch('loginUser', result.access_token) + this.$router.push({ name: 'friends' }) + }) } }, } diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js index 5a54d846b..b2e6e1093 100644 --- a/src/components/password_reset/password_reset.js +++ b/src/components/password_reset/password_reset.js @@ -1,10 +1,10 @@ import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'vuex' -import passwordResetApi from '../../services/new_api/password_reset.js' - import { useInstanceStore } from 'src/stores/instance.js' +import { resetPassword } from 'src/api/public.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -24,7 +24,7 @@ const passwordReset = { ...mapState({ signedIn: (state) => !!state.users.currentUser, }), - ...mapPiniaState(useInstanceStore, ['server', 'mailerEnabled']), + ...mapPiniaState(useInstanceStore, ['mailerEnabled']), }, created() { if (this.signedIn) { @@ -44,9 +44,8 @@ const passwordReset = { submit() { this.isPending = true const email = this.user.email - const server = this.server - passwordResetApi({ server, email }) + resetPassword({ email }) .then(({ status }) => { this.isPending = false this.user.email = '' diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 7eb129430..6cb99a3b6 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -1,7 +1,6 @@ import { debounce, map, reject, uniqBy } from 'lodash' import { mapActions, mapState } from 'pinia' import { defineAsyncComponent } from 'vue' -import { mapGetters } from 'vuex' import Attachment from 'src/components/attachment/attachment.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' @@ -635,11 +634,7 @@ const PostStatusForm = { // Don't apply preview if not loading, because it means // user has closed the preview manually. if (!this.previewLoading) return - if (!data.error) { - this.preview = data - } else { - this.preview = { error: data.error } - } + this.preview = data }) .catch((error) => { this.preview = { error } diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index 6011281cf..973c2b1a5 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -1,5 +1,4 @@ import { get } from 'lodash' -import { defineAsyncComponent } from 'vue' import Modal from 'src/components/modal/modal.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' diff --git a/src/components/quote/quote.js b/src/components/quote/quote.js index 3b771b3f2..439d3440a 100644 --- a/src/components/quote/quote.js +++ b/src/components/quote/quote.js @@ -1,5 +1,3 @@ -import { defineAsyncComponent } from 'vue' - import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/remote_user_resolver/remote_user_resolver.js b/src/components/remote_user_resolver/remote_user_resolver.js index 430f56c84..75304fe52 100644 --- a/src/components/remote_user_resolver/remote_user_resolver.js +++ b/src/components/remote_user_resolver/remote_user_resolver.js @@ -1,3 +1,7 @@ +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchUser } from 'src/api/public.js' + const RemoteUserResolver = { data: () => ({ error: false, @@ -7,11 +11,12 @@ const RemoteUserResolver = { }, methods: { redirect() { - const acct = - this.$route.params.username + '@' + this.$route.params.hostname - this.$store.state.api.backendInteractor - .fetchUser({ id: acct }) - .then((externalUser) => { + const id = this.$route.params.username + '@' + this.$route.params.hostname + fetchUser({ + id, + credentials: useOAuthStore().token, + }) + .then(({ data: externalUser }) => { if (externalUser.error) { this.error = true } else { diff --git a/src/components/settings_modal/admin_tabs/admin_user_card.js b/src/components/settings_modal/admin_tabs/admin_user_card.js index a075db010..ba0f0e9f7 100644 --- a/src/components/settings_modal/admin_tabs/admin_user_card.js +++ b/src/components/settings_modal/admin_tabs/admin_user_card.js @@ -1,5 +1,3 @@ -import { defineAsyncComponent } from 'vue' - import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue' diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js index 98c0cb467..56361587d 100644 --- a/src/components/settings_modal/admin_tabs/emoji_tab.js +++ b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -2,7 +2,7 @@ import Checkbox from 'components/checkbox/checkbox.vue' import Popover from 'components/popover/popover.vue' import Select from 'components/select/select.vue' import StillImage from 'components/still-image/still-image.vue' -import { assign, clone } from 'lodash' +import { clone } from 'lodash' import { defineAsyncComponent } from 'vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' @@ -11,6 +11,7 @@ import ModifiedIndicator from '../helpers/modified_indicator.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import StringSetting from '../helpers/string_setting.vue' +import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useEmojiStore } from 'src/stores/emoji.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInterfaceStore } from 'src/stores/interface.js' @@ -98,10 +99,10 @@ const EmojiTab = { methods: { reloadEmoji() { - this.$store.state.api.backendInteractor.reloadEmoji() + useAdminSettingsStore().reloadEmoji() }, importFromFS() { - this.$store.state.api.backendInteractor.importEmojiFromFS() + useAdminSettingsStore().importEmojiFromFS() }, emojiAddr(name) { if (this.pack.remote !== undefined) { @@ -113,7 +114,7 @@ const EmojiTab = { }, createEmojiPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .createEmojiPack({ name: this.newPackName }) .then((resp) => resp.json()) .then((resp) => { @@ -130,7 +131,7 @@ const EmojiTab = { }) }, deleteEmojiPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .deleteEmojiPack({ name: this.packName }) .then((resp) => resp.json()) .then((resp) => { @@ -157,7 +158,7 @@ const EmojiTab = { return edited !== def }, savePackMetadata() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }) .then((resp) => resp.json()) .then((resp) => { @@ -182,7 +183,7 @@ const EmojiTab = { useEmojiStore() .getAdminPacks( this.remotePackInstance, - this.$store.state.api.backendInteractor.listEmojiPacks, + useAdminSettingsStore().listEmojiPacks, ) .then((allPacks) => { this.knownLocalPacks = allPacks @@ -195,7 +196,7 @@ const EmojiTab = { useEmojiStore() .getAdminPacks( this.remotePackInstance, - this.$store.state.api.backendInteractor.listRemoteEmojiPacks, + useAdminSettingsStore().listRemoteEmojiPacks, ) .then((allPacks) => { let inst = this.remotePackInstance @@ -226,7 +227,7 @@ const EmojiTab = { this.remotePackDownloadAs = this.pack.remote.baseName } - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPack({ instance: this.pack.remote.instance, packName: this.pack.remote.baseName, @@ -247,7 +248,7 @@ const EmojiTab = { }) }, downloadRemoteURLPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPackZIP({ url: this.remotePackURL, packName: this.newPackName, @@ -268,7 +269,7 @@ const EmojiTab = { }) }, downloadRemoteFilePack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPackZIP({ file: this.remotePackFile[0], packName: this.newPackName, diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js index 9bec3d763..b2e6c10d0 100644 --- a/src/components/settings_modal/admin_tabs/frontends_tab.js +++ b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -71,7 +71,7 @@ const FrontendsTab = { const payload = { name, ref } this.working = true - this.$store.state.api.backendInteractor + useAdminSettingsStore() .installFrontend({ payload }) .finally(() => { this.working = false diff --git a/src/components/settings_modal/admin_tabs/users_tab.js b/src/components/settings_modal/admin_tabs/users_tab.js index ab40c723a..68bc46aa2 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.js +++ b/src/components/settings_modal/admin_tabs/users_tab.js @@ -1,5 +1,3 @@ -import { isEmpty } from 'lodash' - import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import List from 'src/components/list/list.vue' diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue index 7078d2488..d3db657d3 100644 --- a/src/components/settings_modal/helpers/emoji_editing_popover.vue +++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -153,6 +153,14 @@ import Popover from 'components/popover/popover.vue' import SelectComponent from 'components/select/select.vue' import { defineAsyncComponent } from 'vue' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + addNewEmojiFile, + deleteEmojiFile, + updateEmojiFile, +} from 'src/api/admin.js' + export default { components: { Popover, @@ -243,14 +251,14 @@ export default { saveEditedEmoji() { if (!this.isEdited) return - this.$store.state.api.backendInteractor - .updateEmojiFile({ - packName: this.packName, - shortcode: this.shortcode, - newShortcode: this.editedShortcode, - newFilename: this.editedFile, - force: false, - }) + updateEmojiFile({ + packName: this.packName, + shortcode: this.shortcode, + newShortcode: this.editedShortcode, + newFilename: this.editedFile, + force: false, + credentials: useOAuthStore().token, + }) .then((resp) => { if (resp.error !== undefined) { this.$emit('displayError', resp.error) @@ -263,18 +271,18 @@ export default { }, uploadEmoji() { let packName = this.remote === undefined ? this.packName : this.copyToPack - this.$store.state.api.backendInteractor - .addNewEmojiFile({ - packName: packName, - file: - this.remote === undefined - ? this.uploadURL !== '' - ? this.uploadURL - : this.uploadFile[0] - : this.emojiAddr(this.file), - shortcode: this.editedShortcode, - filename: this.editedFile, - }) + addNewEmojiFile({ + packName: packName, + file: + this.remote === undefined + ? this.uploadURL !== '' + ? this.uploadURL + : this.uploadFile[0] + : this.emojiAddr(this.file), + shortcode: this.editedShortcode, + filename: this.editedFile, + credentials: useOAuthStore().token, + }) .then((resp) => resp.json()) .then((resp) => { if (resp.error !== undefined) { @@ -297,8 +305,11 @@ export default { deleteEmoji() { this.deleteModalVisible = false - this.$store.state.api.backendInteractor - .deleteEmojiFile({ packName: this.packName, shortcode: this.shortcode }) + deleteEmojiFile({ + packName: this.packName, + shortcode: this.shortcode, + credentials: useOAuthStore().token, + }) .then((resp) => resp.json()) .then((resp) => { if (resp.error !== undefined) { diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index b9ff08bb5..3b76708fe 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -5,7 +5,6 @@ import LocalSettingIndicator from './local_setting_indicator.vue' import ModifiedIndicator from './modified_indicator.vue' import { useAdminSettingsStore } from 'src/stores/admin_settings.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' diff --git a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx index cbe1be158..61ed6bee1 100644 --- a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx +++ b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx @@ -1,8 +1,7 @@ // eslint-disable-next-line no-unused -import { throttle } from 'lodash' -import { mapState as mapPiniaState, mapState } from 'pinia' -import { Fragment, h } from 'vue' +import { mapState as mapPiniaState } from 'pinia' +import { Fragment } from 'vue' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js index fb2d26778..22ba0e16c 100644 --- a/src/components/settings_modal/settings_modal_admin_content.js +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -1,4 +1,3 @@ -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import AuthTab from './admin_tabs/auth_tab.vue' import EmojiTab from './admin_tabs/emoji_tab.vue' import FederationTab from './admin_tabs/federation_tab.vue' diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js index 68d2b923a..c11579e53 100644 --- a/src/components/settings_modal/tabs/appearance_tab.js +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -12,7 +12,9 @@ import Preview from './old_theme_tab/theme_preview.vue' import { useInstanceStore } from 'src/stores/instance.js' import { normalizeThemeData, useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { updateProfileImages } from 'src/api/user.js' import { newImporter } from 'src/services/export_import/export_import.js' import { adoptStyleSheets, @@ -484,9 +486,11 @@ const AppearanceTab = { } this.backgroundUploading = true - this.$store.state.api.backendInteractor - .updateProfileImages({ background }) - .then((data) => { + updateProfileImages({ + background, + credentials: useOAuthStore().token, + }) + .then(({ data }) => { this.$store.commit('addNewUsers', [data]) this.$store.commit('setCurrentUser', data) this.backgroundPreview = null diff --git a/src/components/settings_modal/tabs/composing_tab.js b/src/components/settings_modal/tabs/composing_tab.js index ab8d0101d..1586c87bf 100644 --- a/src/components/settings_modal/tabs/composing_tab.js +++ b/src/components/settings_modal/tabs/composing_tab.js @@ -14,8 +14,10 @@ import UnitSetting from '../helpers/unit_setting.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { updateProfile } from 'src/api/user.js' import localeService from 'src/services/locale/locale.service.js' import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js' @@ -164,12 +166,13 @@ const ComposingTab = { ), } - this.$store.state.api.backendInteractor - .updateProfile({ params }) - .then((user) => { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', user) - }) + updateProfile({ + params, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + }) }, updateFont(key, value) { useSyncConfigStore().setSimplePrefAndSave({ diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js index cc41302c7..4554f4a1f 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.js +++ b/src/components/settings_modal/tabs/data_import_export_tab.js @@ -4,8 +4,20 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import Exporter from 'src/components/exporter/exporter.vue' import Importer from 'src/components/importer/importer.vue' +import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js' +import { + addBackup, + exportFriends, + fetchBlocks, + fetchMutes, + importBlocks, + importFollows, + importMutes, + listBackups, +} from 'src/api/user.js' + const DataImportExportTab = { data() { return { @@ -28,48 +40,57 @@ const DataImportExportTab = { }, computed: { ...mapState({ - backendInteractor: (state) => state.api.backendInteractor, user: (state) => state.users.currentUser, }), }, methods: { getFollowsContent() { - return this.backendInteractor - .exportFriends({ id: this.user.id }) - .then(this.generateExportableUsersContent) + return exportFriends({ + id: this.user.id, + credentials: useOAuthStore().token, + }).then(this.generateExportableUsersContent) }, getBlocksContent() { - return this.backendInteractor - .fetchBlocks() - .then(this.generateExportableUsersContent) + return fetchBlocks({ + credentials: useOAuthStore().token, + }).then(this.generateExportableUsersContent) }, getMutesContent() { - return this.backendInteractor - .fetchMutes() - .then(this.generateExportableUsersContent) + return fetchMutes({ + credentials: useOAuthStore().token, + }).then(this.generateExportableUsersContent) }, importFollows(file) { - return this.backendInteractor.importFollows({ file }).then((status) => { + return importFollows({ + file, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { if (!status) { throw new Error('failed') } }) }, importBlocks(file) { - return this.backendInteractor.importBlocks({ file }).then((status) => { + return importBlocks({ + file, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { if (!status) { throw new Error('failed') } }) }, importMutes(file) { - return this.backendInteractor.importMutes({ file }).then((status) => { + return importMutes({ + file, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { if (!status) { throw new Error('failed') } }) }, - generateExportableUsersContent(users) { + generateExportableUsersContent({ data: users }) { // Get addresses return users .map((user) => { @@ -83,8 +104,9 @@ const DataImportExportTab = { .join('\n') }, addBackup() { - this.$store.state.api.backendInteractor - .addBackup() + addBackup({ + credentials: useOAuthStore().token, + }) .then(() => { this.addedBackup = true this.addBackupError = false @@ -96,9 +118,10 @@ const DataImportExportTab = { .then(() => this.fetchBackups()) }, fetchBackups() { - this.$store.state.api.backendInteractor - .listBackups() - .then((res) => { + listBackups({ + credentials: useOAuthStore().token, + }) + .then(({ data: res }) => { this.backups = res this.listBackupsError = false }) diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index db1177cb2..3ab3846d2 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -8,12 +8,13 @@ import FloatSetting from '../helpers/float_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import UnitSetting from '../helpers/unit_setting.vue' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { updateProfile } from 'src/api/user.js' import localeService from 'src/services/locale/locale.service.js' const GeneralTab = { @@ -58,12 +59,13 @@ const GeneralTab = { ), } - this.$store.state.api.backendInteractor - .updateProfile({ params }) - .then((user) => { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', user) - }) + updateProfile({ + params, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + }) }, updateFont(path, value) { useLocalConfigStore().set({ path, value }) diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 0d889ce54..c45d6ad76 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -1,4 +1,4 @@ -import { get, isEmpty, map, reject } from 'lodash' +import { get, map, reject } from 'lodash' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import BlockCard from 'src/components/block_card/block_card.vue' @@ -10,8 +10,11 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import { useInstanceStore } from 'src/stores/instance.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js' +import { importBlocks, importFollows } from 'src/api/user.js' + const MutesAndBlocks = { data() { return { @@ -54,22 +57,24 @@ const MutesAndBlocks = { return () => this.$store.dispatch('fetch' + group, this.userId) }, importFollows(file) { - return this.$store.state.api.backendInteractor - .importFollows({ file }) - .then((status) => { - if (!status) { - throw new Error('failed') - } - }) + return importFollows({ + file, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + if (!status) { + throw new Error('failed') + } + }) }, importBlocks(file) { - return this.$store.state.api.backendInteractor - .importBlocks({ file }) - .then((status) => { - if (!status) { - throw new Error('failed') - } - }) + return importBlocks({ + file, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + if (!status) { + throw new Error('failed') + } + }) }, generateExportableUsersContent(users) { // Get addresses diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js index 5114012a8..a666d42e3 100644 --- a/src/components/settings_modal/tabs/notifications_tab.js +++ b/src/components/settings_modal/tabs/notifications_tab.js @@ -1,6 +1,10 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateNotificationSettings } from 'src/api/user.js' + const NotificationsTab = { data() { return { @@ -27,7 +31,8 @@ const NotificationsTab = { }, methods: { updateNotificationSettings() { - this.$store.state.api.backendInteractor.updateNotificationSettings({ + updateNotificationSettings({ + credentials: useOAuthStore().token, settings: this.notificationSettings, }) }, diff --git a/src/components/settings_modal/tabs/posts_tab.js b/src/components/settings_modal/tabs/posts_tab.js index dbf284d93..640ba5084 100644 --- a/src/components/settings_modal/tabs/posts_tab.js +++ b/src/components/settings_modal/tabs/posts_tab.js @@ -6,7 +6,6 @@ import SharedComputedObject from '../helpers/shared_computed_object.js' import UnitSetting from '../helpers/unit_setting.vue' import { useLocalConfigStore } from 'src/stores/local_config.js' -import { useSyncConfigStore } from 'src/stores/sync_config.js' const PostsTab = { data() { diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 3e12f265f..844e223c9 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -3,6 +3,10 @@ import UserCard from 'src/components/user_card/user_card.vue' import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateProfile } from 'src/api/user.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch, @@ -35,10 +39,11 @@ const ProfileTab = { const params = { locked: this.locked, } - - this.$store.state.api.backendInteractor - .updateProfile({ params }) - .then((user) => { + updateProfile({ + params, + credentials: useOAuthStore().token, + }) + .then(({ data: user }) => { this.$store.commit('addNewUsers', [user]) this.$store.commit('setCurrentUser', user) }) diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js index a998fdb4b..1277b42ec 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa.js +++ b/src/components/settings_modal/tabs/security_tab/mfa.js @@ -1,10 +1,18 @@ import VueQrcode from '@chenfengyuan/vue-qrcode' -import { mapState } from 'vuex' import Confirm from './confirm.vue' import RecoveryCodes from './mfa_backup_codes.vue' import TOTP from './mfa_totp.vue' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + generateMfaBackupCodes, + mfaConfirmOTP, + mfaSetupOTP, + settingsMFA, +} from 'src/api/user.js' + const Mfa = { data: () => ({ settings: { @@ -71,9 +79,6 @@ const Mfa = { confirmNewBackupCodes() { return this.backupCodes.getNewCodes }, - ...mapState({ - backendInteractor: (state) => state.api.backendInteractor, - }), }, methods: { @@ -87,7 +92,9 @@ const Mfa = { this.backupCodes.inProgress = true this.backupCodes.codes = [] - return this.backendInteractor.generateMfaBackupCodes().then((res) => { + return generateMfaBackupCodes({ + credentials: useOAuthStore().token, + }).then(({ data: res }) => { this.backupCodes.codes = res.codes this.backupCodes.inProgress = false }) @@ -112,7 +119,9 @@ const Mfa = { // prepare setup OTP this.setupState.state = 'setupOTP' this.setupState.setupOTPState = 'prepare' - this.backendInteractor.mfaSetupOTP().then((res) => { + mfaSetupOTP({ + credentials: useOAuthStore().token, + }).then(({ data: res }) => { this.otpSettings = res this.setupState.setupOTPState = 'confirm' }) @@ -120,18 +129,17 @@ const Mfa = { doConfirmOTP() { // handler confirm enable OTP this.error = null - this.backendInteractor - .mfaConfirmOTP({ - token: this.otpConfirmToken, - password: this.currentPassword, - }) - .then((res) => { - if (res.error) { - this.error = res.error - return - } + mfaConfirmOTP({ + token: this.otpConfirmToken, + password: this.currentPassword, + credentials: useOAuthStore().token, + }) + .then(() => { this.completeSetup() }) + .catch((error) => { + this.error = error + }) }, completeSetup() { @@ -152,7 +160,9 @@ const Mfa = { // fetch settings from server async fetchSettings() { - const result = await this.backendInteractor.settingsMFA() + const { data: result } = await settingsMFA({ + credentials: useOAuthStore().token, + }) if (result.error) return this.settings = result.settings this.settings.available = true diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js index e011fd638..2c4c194bc 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa_totp.js +++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.js @@ -1,7 +1,9 @@ -import { mapState } from 'vuex' - import Confirm from './confirm.vue' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { mfaDisableOTP } from 'src/api/user.js' + export default { props: ['settings'], data: () => ({ @@ -17,9 +19,6 @@ export default { isActivated() { return this.settings.totp }, - ...mapState({ - backendInteractor: (state) => state.api.backendInteractor, - }), }, methods: { doActivate() { @@ -36,19 +35,18 @@ export default { // confirm deactivate TOTP method this.error = null this.inProgress = true - this.backendInteractor - .mfaDisableOTP({ - password: this.currentPassword, - }) - .then((res) => { - this.inProgress = false - if (res.error) { - this.error = res.error - return - } - this.deactivate = false - this.$emit('deactivate') - }) + mfaDisableOTP({ + password: this.currentPassword, + credentials: useOAuthStore().token, + }).then(({ data: res }) => { + this.inProgress = false + if (res.error) { + this.error = res.error + return + } + this.deactivate = false + this.$emit('deactivate') + }) }, }, } diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js index 96510edcf..a2571bd54 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.js +++ b/src/components/settings_modal/tabs/security_tab/security_tab.js @@ -2,10 +2,19 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue' import Mfa from './mfa.vue' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthTokensStore } from 'src/stores/oauth_tokens' +import { + addAlias, + changeEmail, + changePassword, + deleteAccount, + deleteAlias, + listAliases, + moveAccount, +} from 'src/api/user.js' import localeService from 'src/services/locale/locale.service.js' const SecurityTab = { @@ -65,78 +74,79 @@ const SecurityTab = { this.deletingAccount = true }, deleteAccount() { - this.$store.state.api.backendInteractor - .deleteAccount({ password: this.deleteAccountConfirmPasswordInput }) - .then((res) => { - if (res.status === 'success') { - this.$store.dispatch('logout') - this.$router.push({ name: 'root' }) - } else { - this.deleteAccountError = res.error - } - }) + deleteAccount({ + credentials: useOAuthStore().token, + password: this.deleteAccountConfirmPasswordInput, + }).then(({ data: res }) => { + if (res.status === 'success') { + this.$store.dispatch('logout') + this.$router.push({ name: 'root' }) + } else { + this.deleteAccountError = res.error + } + }) }, changePassword() { const params = { password: this.changePasswordInputs[0], newPassword: this.changePasswordInputs[1], newPasswordConfirmation: this.changePasswordInputs[2], + credentials: useOAuthStore().token, } - this.$store.state.api.backendInteractor - .changePassword(params) - .then((res) => { - if (res.status === 'success') { - this.changedPassword = true - this.changePasswordError = false - this.logout() - } else { - this.changedPassword = false - this.changePasswordError = res.error - } - }) + changePassword(params).then(({ data: res }) => { + if (res.status === 'success') { + this.changedPassword = true + this.changePasswordError = false + this.logout() + } else { + this.changedPassword = false + this.changePasswordError = res.error + } + }) }, changeEmail() { const params = { email: this.newEmail, password: this.changeEmailPassword, + credentials: useOAuthStore().token, } - this.$store.state.api.backendInteractor - .changeEmail(params) - .then((res) => { - if (res.status === 'success') { - this.changedEmail = true - this.changeEmailError = false - } else { - this.changedEmail = false - this.changeEmailError = res.error - } - }) + changeEmail(params).then(({ data: res }) => { + if (res.status === 'success') { + this.changedEmail = true + this.changeEmailError = false + } else { + this.changedEmail = false + this.changeEmailError = res.error + } + }) }, moveAccount() { const params = { targetAccount: this.moveAccountTarget, password: this.moveAccountPassword, + credentials: useOAuthStore().token, } - this.$store.state.api.backendInteractor - .moveAccount(params) - .then((res) => { - if (res.status === 'success') { - this.movedAccount = true - this.moveAccountError = false - } else { - this.movedAccount = false - this.moveAccountError = res.error - } - }) + moveAccount(params).then(({ data: res }) => { + if (res.status === 'success') { + this.movedAccount = true + this.moveAccountError = false + } else { + this.movedAccount = false + this.moveAccountError = res.error + } + }) }, removeAlias(alias) { - this.$store.state.api.backendInteractor - .deleteAlias({ alias }) - .then(() => this.fetchAliases()) + deleteAlias({ + alias, + credentials: useOAuthStore().token, + }).then(() => this.fetchAliases()) }, addAlias() { - this.$store.state.api.backendInteractor - .addAlias({ alias: this.addAliasTarget }) + addAlias({ + alias: this.addAliasTarget, + credentials: useOAuthStore().token, + }) .then(() => { this.addedAlias = true this.addAliasError = false @@ -149,9 +159,10 @@ const SecurityTab = { .then(() => this.fetchAliases()) }, fetchAliases() { - this.$store.state.api.backendInteractor - .listAliases() - .then((res) => { + listAliases({ + credentials: useOAuthStore().token, + }) + .then(({ data: res }) => { this.aliases = res.aliases this.listAliasesError = false }) diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 840beeb7c..27e9a5249 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -1,5 +1,4 @@ import { mapActions, mapState } from 'pinia' -import { defineAsyncComponent } from 'vue' import { mapGetters } from 'vuex' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index 2d0de25ef..e02b71b0c 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -107,7 +107,7 @@ const StatusActionButtons = { button .action?.(this.funcArg) .then(() => this.$emit('onSuccess')) - .catch((err) => this.$emit('onError', err.error.error)) + .catch((err) => this.$emit('onError', err)) }, onExtraClose() { this.showPin = false diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index 0f02bb6fb..2186498ef 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -1,6 +1,5 @@ import { mapState as mapPiniaState } from 'pinia' -import { defineAsyncComponent } from 'vue' -import { mapGetters, mapState } from 'vuex' +import { mapState } from 'vuex' import Attachment from 'src/components/attachment/attachment.vue' import Gallery from 'src/components/gallery/gallery.vue' diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js index fe0056305..95da91ba7 100644 --- a/src/components/status_popover/status_popover.js +++ b/src/components/status_popover/status_popover.js @@ -1,7 +1,6 @@ import { find } from 'lodash' import Popover from 'src/components/popover/popover.vue' -import Status from 'src/components/status/status.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js index 949d2ecf9..482aacb81 100644 --- a/src/components/sticker_picker/sticker_picker.js +++ b/src/components/sticker_picker/sticker_picker.js @@ -4,7 +4,6 @@ import statusPosterService from '../../services/status_poster/status_poster.serv import TabSwitcher from '../tab_switcher/tab_switcher.jsx' import { useEmojiStore } from 'src/stores/emoji.js' -import { useInstanceStore } from 'src/stores/instance.js' const StickerPicker = { components: { diff --git a/src/components/still-image/still-image-emoji-popover.js b/src/components/still-image/still-image-emoji-popover.js index 02f036a52..92cc20904 100644 --- a/src/components/still-image/still-image-emoji-popover.js +++ b/src/components/still-image/still-image-emoji-popover.js @@ -2,6 +2,7 @@ import Popover from 'components/popover/popover.vue' import SelectComponent from 'components/select/select.vue' import { mapState } from 'pinia' +import { useAdminSettingsStore } from 'src/stores/admin_settings' import { useEmojiStore } from 'src/stores/emoji' import { useInterfaceStore } from 'src/stores/interface' @@ -37,7 +38,7 @@ export default { }) }, copyToLocalPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .addNewEmojiFile({ packName: this.packName, file: this.$attrs.src, diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index b485b3ff8..a3c5b61cd 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -1,14 +1,11 @@ // eslint-disable-next-line no-unused -import { mapState } from 'pinia' -import { Fragment, h } from 'vue' +import { Fragment } from 'vue' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import './tab_switcher.scss' -import { useInterfaceStore } from 'src/stores/interface.js' - const findFirstUsable = (slots) => slots.findIndex((_) => _.props) export default { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 3c9f55725..31a513130 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -25,6 +25,7 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js' import { usePostStatusStore } from 'src/stores/post_status' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { updateProfile } from 'src/api/user.js' import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js' import localeService from 'src/services/locale/locale.service.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -597,9 +598,8 @@ export default { params.header = this.newBannerFile } - this.$store.state.api.backendInteractor - .updateProfile({ params }) - .then((user) => { + updateProfile({ params }) + .then(({ data: user }) => { this.newFields.splice(this.newFields.length) merge(this.newFields, user.fields) this.$store.commit('addNewUsers', [user]) diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index 8a4612690..7f9ba4ffb 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -1,5 +1,3 @@ -import { defineAsyncComponent } from 'vue' - import Popover from 'src/components/popover/popover.vue' import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue' diff --git a/src/components/user_popover/user_popover.js b/src/components/user_popover/user_popover.js index de354d676..67c75be50 100644 --- a/src/components/user_popover/user_popover.js +++ b/src/components/user_popover/user_popover.js @@ -1,5 +1,4 @@ import { mapState } from 'pinia' -import { defineAsyncComponent } from 'vue' import Popover from 'src/components/popover/popover.vue' import UserCard from 'src/components/user_card/user_card.vue' diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index d2d766a7f..eb1d8e068 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -1,5 +1,4 @@ import { get } from 'lodash' -import { mapState } from 'pinia' import FollowCard from 'src/components/follow_card/follow_card.vue' import List from 'src/components/list/list.vue' @@ -7,7 +6,6 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import Timeline from 'src/components/timeline/timeline.vue' import UserCard from 'src/components/user_card/user_card.vue' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' diff --git a/src/components/user_profile/user_profile_admin_view.js b/src/components/user_profile/user_profile_admin_view.js index 94092b155..da2c6c322 100644 --- a/src/components/user_profile/user_profile_admin_view.js +++ b/src/components/user_profile/user_profile_admin_view.js @@ -1,6 +1,3 @@ -import { get } from 'lodash' -import { mapState } from 'pinia' - import Checkbox from 'src/components/checkbox/checkbox.vue' import List from 'src/components/list/list.vue' import Status from 'src/components/status/status.vue' diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 12d548054..017a17efa 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -5,8 +5,11 @@ import List from 'src/components/list/list.vue' import Modal from 'src/components/modal/modal.vue' import UserLink from 'src/components/user_link/user_link.vue' +import { useOAuthStore } from 'src/stores/oauth.js' import { useReportsStore } from 'src/stores/reports.js' +import { reportUser } from 'src/api/user.js' + const UserReportingModal = { components: { List, @@ -70,9 +73,9 @@ const UserReportingModal = { comment: this.comment, forward: this.forward, statusIds: [...this.statusIdsToReport], + credentials: useOAuthStore().token, } - this.$store.state.api.backendInteractor - .reportUser({ ...params }) + reportUser({ ...params }) .then(() => { this.processing = false this.resetState() diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js index 8ca2d8c1a..28b63c973 100644 --- a/src/components/who_to_follow/who_to_follow.js +++ b/src/components/who_to_follow/who_to_follow.js @@ -1,7 +1,8 @@ import FollowCard from 'src/components/follow_card/follow_card.vue' -import apiService from '../../services/api/api.service.js' -import { useInstanceStore } from 'src/stores/instance.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchUser, suggestions } from 'src/api/public.js' const WhoToFollow = { components: { @@ -17,21 +18,22 @@ const WhoToFollow = { }, methods: { showWhoToFollow(reply) { - reply.forEach((i) => { - this.$store.state.api.backendInteractor - .fetchUser({ id: i.acct }) - .then((externalUser) => { - if (!externalUser.error) { - this.$store.commit('addNewUsers', [externalUser]) - this.users.push(externalUser) - } - }) + reply.forEach(({ id }) => { + fetchUser({ + id, + credentials: useOAuthStore().token, + }).then(({ data: externalUser }) => { + if (!externalUser.error) { + this.$store.commit('addNewUsers', [externalUser]) + this.users.push(externalUser) + } + }) }) }, getWhoToFollow() { - const credentials = this.$store.state.users.currentUser.credentials + const credentials = useOAuthStore().token if (credentials) { - apiService.suggestions({ credentials }).then((reply) => { + suggestions({ credentials }).then(({ data: reply }) => { this.showWhoToFollow(reply) }) } diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js index cd0524ecf..7a65ba39d 100644 --- a/src/components/who_to_follow_panel/who_to_follow_panel.js +++ b/src/components/who_to_follow_panel/who_to_follow_panel.js @@ -1,10 +1,10 @@ import { shuffle } from 'lodash' -import apiService from '../../services/api/api.service.js' - import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { fetchUser, suggestions } from 'src/api/public.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' function showWhoToFollow(panel, reply) { @@ -18,14 +18,15 @@ function showWhoToFollow(panel, reply) { toFollow.img = img toFollow.name = name - panel.$store.state.api.backendInteractor - .fetchUser({ id: name }) - .then((externalUser) => { - if (!externalUser.error) { - panel.$store.commit('addNewUsers', [externalUser]) - toFollow.id = externalUser.id - } - }) + fetchUser({ + id: name, + credentials: useOAuthStore().token, + }).then(({ data: externalUser }) => { + if (!externalUser.error) { + panel.$store.commit('addNewUsers', [externalUser]) + toFollow.id = externalUser.id + } + }) }) } @@ -35,7 +36,7 @@ function getWhoToFollow(panel) { panel.usersToFollow.forEach((toFollow) => { toFollow.name = 'Loading...' }) - apiService.suggestions({ credentials }).then((reply) => { + suggestions({ credentials }).then(({ data: reply }) => { showWhoToFollow(panel, reply) }) } diff --git a/src/lib/language.js b/src/lib/language.js index af53eeffd..dc5121452 100644 --- a/src/lib/language.js +++ b/src/lib/language.js @@ -1,5 +1,3 @@ -import Cookies from 'js-cookie' - import { useEmojiStore } from 'src/stores/emoji.js' import { useI18nStore } from 'src/stores/i18n.js' diff --git a/src/modules/api.js b/src/modules/api.js index 6f4b8b15f..72d7fb4d7 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,20 +1,27 @@ import { Socket } from 'phoenix' -import { WSConnectionStatus } from '../services/api/api.service.js' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' 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/timelines.js' +import { + getMastodonSocketURI, + ProcessedWS, + WSConnectionStatus, +} 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' + const retryTimeout = (multiplier) => 1000 * multiplier const api = { state: { retryMultiplier: 1, - backendInteractor: backendInteractorService(), fetchers: {}, socket: null, mastoUserSocket: null, @@ -25,9 +32,6 @@ const api = { followRequestCount: (state) => state.followRequests.length, }, mutations: { - setBackendInteractor(state, backendInteractor) { - state.backendInteractor = backendInteractor - }, addFetcher(state, { fetcherName, fetcher }) { state.fetchers[fetcherName] = fetcher }, @@ -91,9 +95,16 @@ const api = { try { const { state, commit, dispatch, rootState } = store const timelineData = rootState.statuses.timelines.friends - state.mastoUserSocket = state.backendInteractor.startUserSocket({ - store, + + const credentials = useOAuthStore().token + const url = getMastodonSocketURI({ credentials }) + + state.mastoUserSocket = ProcessedWS({ + url, + id: 'Unified', + credentials, }) + state.mastoUserSocket.addEventListener( 'pleroma:authenticated', () => { @@ -245,7 +256,7 @@ const api = { return if (store.state.fetchers[timeline]) return - const fetcher = store.state.backendInteractor.startFetchingTimeline({ + const fetcher = timelineFetcher.startFetching({ timeline, store, userId, @@ -253,7 +264,9 @@ const api = { statusId, bookmarkFolderId, tag, + credentials: useOAuthStore().token, }) + store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, stopFetchingTimeline(store, timeline) { @@ -261,19 +274,22 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: timeline, fetcher }) }, + fetchTimeline(store, { timeline, ...rest }) { - store.state.backendInteractor.fetchTimeline({ + fetchTimeline({ store, timeline, ...rest, + credentials: useOAuthStore().token, }) }, // Notifications startFetchingNotifications(store) { if (store.state.fetchers.notifications) return - const fetcher = store.state.backendInteractor.startFetchingNotifications({ + const fetcher = notificationsFetcher.startFetching({ store, + credentials: useOAuthStore().token, }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) }, @@ -282,19 +298,14 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, - fetchNotifications(store, { ...rest }) { - store.state.backendInteractor.fetchNotifications({ - store, - ...rest, - }) - }, // Follow requests startFetchingFollowRequests(store) { if (store.state.fetchers.followRequests) return - const fetcher = store.state.backendInteractor.startFetchingFollowRequests( - { store }, - ) + const fetcher = followRequestFetcher.startFetchingFollowRequests({ + store, + credentials: useOAuthStore().token, + }) store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) }, @@ -303,39 +314,6 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher }) }, - removeFollowRequest(store, request) { - const requests = store.state.followRequests.filter((it) => it !== request) - store.commit('setFollowRequests', requests) - }, - - // Lists - startFetchingLists(store) { - if (store.state.fetchers.lists) return - const fetcher = store.state.backendInteractor.startFetchingLists({ - store, - }) - store.commit('addFetcher', { fetcherName: 'lists', fetcher }) - }, - stopFetchingLists(store) { - const fetcher = store.state.fetchers.lists - if (!fetcher) return - store.commit('removeFetcher', { fetcherName: 'lists', fetcher }) - }, - - // Bookmark folders - startFetchingBookmarkFolders(store) { - if (store.state.fetchers.bookmarkFolders) return - if (!useInstanceCapabilitiesStore().pleromaBookmarkFoldersAvailable) - return - const fetcher = - store.state.backendInteractor.startFetchingBookmarkFolders({ store }) - store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher }) - }, - stopFetchingBookmarkFolders(store) { - const fetcher = store.state.fetchers.bookmarkFolders - if (!fetcher) return - store.commit('removeFetcher', { fetcherName: 'bookmarkFolders', fetcher }) - }, // Pleroma websocket setWsToken(store, token) { diff --git a/src/modules/chats.js b/src/modules/chats.js index 308b2cb27..abc035f09 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -9,6 +9,10 @@ import { } from '../services/entity_normalizer/entity_normalizer.service.js' import { promiseInterval } from '../services/promise_interval/promise_interval.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { chats, deleteChatMessage, readChat } from 'src/api/chats.js' + const emptyChatList = () => ({ data: [], idStore: {}, @@ -36,7 +40,7 @@ const unreadChatCount = (state) => { return sumBy(state.chatList.data, 'unread') } -const chats = { +const chatsModule = { state: { ...defaultState }, getters: { currentChat: (state) => state.openedChats[state.currentChatId], @@ -51,7 +55,6 @@ const chats = { // Chat list startFetchingChats({ dispatch, commit }) { const fetcher = () => dispatch('fetchChats', { latest: true }) - fetcher() commit('setChatListFetcher', { fetcher: () => promiseInterval(fetcher, 5000), }) @@ -60,8 +63,10 @@ const chats = { commit('setChatListFetcher', { fetcher: undefined }) }, fetchChats({ dispatch, rootState }) { - return rootState.api.backendInteractor.chats().then(({ chats }) => { - dispatch('addNewChats', { chats }) + return chats({ + credentials: useOAuthStore().token, + }).then(({ chatList }) => { + dispatch('addNewChats', { chats: chatList }) return chats }) }, @@ -113,11 +118,18 @@ const chats = { commit('readChat', { id, lastReadId }) if (isNewMessage) { - rootState.api.backendInteractor.readChat({ id, lastReadId }) + readChat({ + id, + lastReadId, + credentials: useOAuthStore().token, + }) } }, deleteChatMessage({ rootState, commit }, value) { - rootState.api.backendInteractor.deleteChatMessage(value) + deleteChatMessage({ + ...value, + credentials: useOAuthStore().token, + }) commit('deleteChatMessage', { commit, ...value }) }, resetChats({ commit, dispatch }) { @@ -262,4 +274,4 @@ const chats = { }, } -export default chats +export default chatsModule diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js index 6a970ef0f..ec190af28 100644 --- a/src/modules/default_config_state.js +++ b/src/modules/default_config_state.js @@ -1,4 +1,4 @@ -import { get, set } from 'lodash' +import { get } from 'lodash' const browserLocale = (navigator.language || 'en').split('-')[0] diff --git a/src/modules/notifications.js b/src/modules/notifications.js index d501b39db..b77683811 100644 --- a/src/modules/notifications.js +++ b/src/modules/notifications.js @@ -1,4 +1,3 @@ -import apiService from '../services/api/api.service.js' import { closeAllDesktopNotifications, closeDesktopNotification, @@ -11,9 +10,12 @@ import { import { useI18nStore } from 'src/stores/i18n.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useReportsStore } from 'src/stores/reports.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js' + const emptyNotifications = () => ({ desktopNotificationSilence: true, maxId: 0, @@ -154,33 +156,32 @@ export const notifications = { }, markNotificationsAsSeen({ rootState, state, commit }) { commit('markNotificationsAsSeen') - apiService - .markNotificationsAsSeen({ - id: state.maxId, - credentials: rootState.users.currentUser.credentials, - }) - .then(() => { - closeAllDesktopNotifications(rootState) - }) + markNotificationsAsSeen({ + id: state.maxId, + credentials: rootState.users.currentUser.credentials, + }).then(() => { + closeAllDesktopNotifications(rootState) + }) }, markSingleNotificationAsSeen({ rootState, commit }, { id }) { commit('markSingleNotificationAsSeen', { id }) - apiService - .markNotificationsAsSeen({ - single: true, - id, - credentials: rootState.users.currentUser.credentials, - }) - .then(() => { - closeDesktopNotification(rootState, { id }) - }) + markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials, + }).then(() => { + closeDesktopNotification(rootState, { id }) + }) }, dismissNotificationLocal({ commit }, { id }) { commit('dismissNotification', { id }) }, dismissNotification({ rootState, commit }, { id }) { commit('dismissNotification', { id }) - rootState.api.backendInteractor.dismissNotification({ id }) + dismissNotification({ + id, + credentials: useOAuthStore().token, + }) }, updateNotification({ commit }, { id, updater }) { commit('updateNotification', { id, updater }) diff --git a/src/modules/profileConfig.js b/src/modules/profileConfig.js index 90571e21d..3fca7d6a8 100644 --- a/src/modules/profileConfig.js +++ b/src/modules/profileConfig.js @@ -1,28 +1,34 @@ import { get, set } from 'lodash' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateNotificationSettings, updateProfile } from 'src/api/user.js' + const defaultApi = ({ rootState, commit }, { path, value }) => { const params = {} set(params, path, value) - return rootState.api.backendInteractor - .updateProfile({ params }) - .then((result) => { - commit('addNewUsers', [result]) - commit('setCurrentUser', result) - }) + return updateProfile({ + params, + credentials: useOAuthStore().token, + }).then(({ data: result }) => { + commit('addNewUsers', [result]) + commit('setCurrentUser', result) + }) } const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { const settings = {} set(settings, path, value) - return rootState.api.backendInteractor - .updateNotificationSettings({ settings }) - .then((result) => { - if (result.status === 'success') { - commit('confirmProfileOption', { name, value }) - } else { - commit('confirmProfileOption', { name, value: oldValue }) - } - }) + return updateNotificationSettings({ + settings, + credentials: useOAuthStore().token, + }).then(({ data: result }) => { + if (result.status === 'success') { + commit('confirmProfileOption', { name, value }) + } else { + commit('confirmProfileOption', { name, value: oldValue }) + } + }) } /** diff --git a/src/modules/statuses.js b/src/modules/statuses.js index b4f0a93bf..445d1d436 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -13,10 +13,36 @@ import { slice, } from 'lodash' -import apiService from '../services/api/api.service.js' - import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + fetchEmojiReactions, + fetchFavoritedByUsers, + fetchPinnedStatuses, + fetchRebloggedByUsers, + fetchScrobbles, + fetchStatus, + fetchStatusHistory, + fetchStatusSource, + search2, +} from 'src/api/public.js' +import { + bookmarkStatus, + deleteStatus, + favorite, + muteConversation, + pinOwnStatus, + reactWithEmoji, + retweet, + unbookmarkStatus, + unfavorite, + unmuteConversation, + unpinOwnStatus, + unreactWithEmoji, + unretweet, +} from 'src/api/user.js' const emptyTl = (userId = 0) => ({ statuses: [], @@ -131,9 +157,8 @@ const getLatestScrobble = (state, user) => { state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000 if (!scrobblesSupport) return - apiService - .fetchScrobbles({ accountId: user.id }) - .then((scrobbles) => { + fetchScrobbles({ accountId: user.id }) + .then(({ data: scrobbles }) => { if (scrobbles?.error) { useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false) return @@ -602,25 +627,24 @@ const statuses = { }) }, fetchStatus({ rootState, dispatch }, id) { - return rootState.api.backendInteractor - .fetchStatus({ id }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return fetchStatus({ id }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, fetchStatusSource({ rootState }, status) { - return apiService.fetchStatusSource({ + return fetchStatusSource({ id: status.id, - credentials: rootState.users.currentUser.credentials, - }) + credentials: useOAuthStore().token, + }).then(({ data }) => data) }, fetchStatusHistory(_, status) { - return apiService.fetchStatusHistory({ status }) + return fetchStatusHistory({ status }).then(({ data }) => data) }, deleteStatus({ rootState, commit }, status) { - apiService - .deleteStatus({ - id: status.id, - credentials: rootState.users.currentUser.credentials, - }) + deleteStatus({ + id: status.id, + credentials: useOAuthStore().token, + }) .then(() => { commit('setDeleted', { status }) }) @@ -643,99 +667,115 @@ const statuses = { favorite({ rootState, commit }, status) { // Optimistic favoriting... commit('setFavorited', { status, value: true }) - rootState.api.backendInteractor - .favorite({ id: status.id }) - .then((status) => - commit('setFavoritedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + favorite({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setFavoritedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, unfavorite({ rootState, commit }, status) { // Optimistic unfavoriting... commit('setFavorited', { status, value: false }) - rootState.api.backendInteractor - .unfavorite({ id: status.id }) - .then((status) => - commit('setFavoritedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + unfavorite({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setFavoritedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, fetchPinnedStatuses({ rootState, dispatch }, userId) { - rootState.api.backendInteractor - .fetchPinnedStatuses({ id: userId }) - .then((statuses) => - dispatch('addNewStatuses', { - statuses, - timeline: 'user', - userId, - showImmediately: true, - noIdUpdate: true, - }), - ) + fetchPinnedStatuses({ + id: userId, + credentials: useOAuthStore().token, + }).then(({ data: statuses }) => + dispatch('addNewStatuses', { + statuses, + timeline: 'user', + userId, + showImmediately: true, + noIdUpdate: true, + }), + ) }, pinStatus({ rootState, dispatch }, statusId) { - return rootState.api.backendInteractor - .pinOwnStatus({ id: statusId }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return pinOwnStatus({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, unpinStatus({ rootState, dispatch }, statusId) { - return rootState.api.backendInteractor - .unpinOwnStatus({ id: statusId }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return unpinOwnStatus({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, muteConversation({ rootState, commit }, { id: statusId }) { - return rootState.api.backendInteractor - .muteConversation({ id: statusId }) - .then((status) => commit('setMutedStatus', status)) + return muteConversation({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => commit('setMutedStatus', status)) }, unmuteConversation({ rootState, commit }, { id: statusId }) { - return rootState.api.backendInteractor - .unmuteConversation({ id: statusId }) - .then((status) => commit('setMutedStatus', status)) + return unmuteConversation({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => commit('setMutedStatus', status)) }, retweet({ rootState, commit }, status) { // Optimistic retweeting... commit('setRetweeted', { status, value: true }) - rootState.api.backendInteractor - .retweet({ id: status.id }) - .then((status) => - commit('setRetweetedConfirm', { - status: status.retweeted_status, - user: rootState.users.currentUser, - }), - ) + retweet({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setRetweetedConfirm', { + status: status.retweeted_status, + user: rootState.users.currentUser, + }), + ) }, unretweet({ rootState, commit }, status) { // Optimistic unretweeting... commit('setRetweeted', { status, value: false }) - rootState.api.backendInteractor - .unretweet({ id: status.id }) - .then((status) => - commit('setRetweetedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + unretweet({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setRetweetedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, bookmark({ rootState, commit }, status) { commit('setBookmarked', { status, value: true }) - rootState.api.backendInteractor - .bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id }) - .then((status) => { - commit('setBookmarkedConfirm', { status }) - }) + bookmarkStatus({ + id: status.id, + folder_id: status.bookmark_folder_id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + commit('setBookmarkedConfirm', { status }) + }) }, unbookmark({ rootState, commit }, status) { commit('setBookmarked', { status, value: false }) - rootState.api.backendInteractor - .unbookmarkStatus({ id: status.id }) - .then((status) => { - commit('setBookmarkedConfirm', { status }) - }) + unbookmarkStatus({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + commit('setBookmarkedConfirm', { status }) + }) }, queueFlush({ commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) @@ -745,8 +785,14 @@ const statuses = { }, fetchFavsAndRepeats({ rootState, commit }, id) { Promise.all([ - rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), - rootState.api.backendInteractor.fetchRebloggedByUsers({ id }), + fetchFavoritedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data }) => data), + fetchRebloggedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data }) => data), ]).then(([favoritedByUsers, rebloggedByUsers]) => { commit('addFavs', { id, @@ -765,7 +811,11 @@ const statuses = { if (!currentUser) return commit('addOwnReaction', { id, emoji, currentUser }) - rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(() => { + reactWithEmoji({ + id, + emoji, + credentials: useOAuthStore().token, + }).then(() => { dispatch('fetchEmojiReactionsBy', id) }) }, @@ -774,59 +824,70 @@ const statuses = { if (!currentUser) return commit('removeOwnReaction', { id, emoji, currentUser }) - rootState.api.backendInteractor - .unreactWithEmoji({ id, emoji }) - .then(() => { - dispatch('fetchEmojiReactionsBy', id) - }) + unreactWithEmoji({ + id, + emoji, + currentUser: rootState.users.currentUser, + }).then(() => { + dispatch('fetchEmojiReactionsBy', id) + }) }, fetchEmojiReactionsBy({ rootState, commit }, id) { - return rootState.api.backendInteractor - .fetchEmojiReactions({ id }) - .then((emojiReactions) => { - commit('addEmojiReactionsBy', { - id, - emojiReactions, - currentUser: rootState.users.currentUser, - }) + return fetchEmojiReactions({ + id, + credentials: useOAuthStore().token, + }).then(({ data: emojiReactions }) => { + commit('addEmojiReactionsBy', { + id, + emojiReactions, + currentUser: rootState.users.currentUser, }) + }) }, fetchFavs({ rootState, commit }, id) { - rootState.api.backendInteractor - .fetchFavoritedByUsers({ id }) - .then((favoritedByUsers) => - commit('addFavs', { - id, - favoritedByUsers, - currentUser: rootState.users.currentUser, - }), - ) + fetchFavoritedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data: favoritedByUsers }) => + commit('addFavs', { + id, + favoritedByUsers, + currentUser: rootState.users.currentUser, + }), + ) }, fetchRepeats({ rootState, commit }, id) { - rootState.api.backendInteractor - .fetchRebloggedByUsers({ id }) - .then((rebloggedByUsers) => - commit('addRepeats', { - id, - rebloggedByUsers, - currentUser: rootState.users.currentUser, - }), - ) + fetchRebloggedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data: rebloggedByUsers }) => + commit('addRepeats', { + id, + rebloggedByUsers, + currentUser: rootState.users.currentUser, + }), + ) }, search(store, { q, resolve, limit, offset, following, type }) { - return store.rootState.api.backendInteractor - .search2({ q, resolve, limit, offset, following, type }) - .then((data) => { - store.commit('addNewUsers', data.accounts) - store.commit( - 'addNewUsers', - data.statuses.map((s) => s.user).filter((u) => u), - ) - data.statuses = store.commit('addNewStatuses', { - statuses: data.statuses, - }) - return data + return search2({ + q, + resolve, + limit, + offset, + following, + type, + credentials: useOAuthStore().token, + }).then(({ data }) => { + store.commit('addNewUsers', data.accounts) + store.commit( + 'addNewUsers', + data.statuses.map((s) => s.user).filter((u) => u), + ) + data.statuses = store.commit('addNewStatuses', { + statuses: data.statuses, }) + return data + }) }, setVirtualHeight({ commit }, { statusId, height }) { commit('setVirtualHeight', { statusId, height }) diff --git a/src/modules/users.js b/src/modules/users.js index ec70f8105..d6b1adaaa 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,3 +1,4 @@ +import Cookies from 'js-cookie' import { compact, concat, @@ -9,9 +10,6 @@ import { uniq, } from 'lodash' -import apiService from '../services/api/api.service.js' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import oauthApi from '../services/new_api/oauth.js' import { registerPushNotifications, unregisterPushNotifications, @@ -21,15 +19,42 @@ import { windowWidth, } from '../services/window_utils/window_utils' +import { useAnnouncementsStore } from 'src/stores/announcements.js' +import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useEmojiStore } from 'src/stores/emoji.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useListsStore } from 'src/stores/lists.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { revokeToken } from 'src/api/oauth.js' +import { + fetchFollowers, + fetchFriends, + fetchUser, + fetchUserByName, + getCaptcha, + register, + searchUsers, + verifyCredentials, +} from 'src/api/public.js' +import { + blockUser as apiBlockUser, + muteUser as apiMuteUser, + unblockUser as apiUnblockUser, + unmuteUser as apiUnmuteUser, + fetchBlocks, + fetchDomainMutes, + fetchMutes, + fetchUserInLists, + fetchUserRelationship, + followUser, +} from 'src/api/user.js' + // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { if (!item) { @@ -72,43 +97,35 @@ const blockUser = (store, args) => { store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addBlockId', id) - return store.rootState.api.backendInteractor - .blockUser({ id, expiresIn }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - store.commit('addBlockId', id) + return apiBlockUser({ id, expiresIn }).then(({ data: relationship }) => { + store.commit('updateUserRelationship', [relationship]) + store.commit('addBlockId', id) - store.commit('removeStatus', { timeline: 'friends', userId: id }) - store.commit('removeStatus', { timeline: 'public', userId: id }) - store.commit('removeStatus', { - timeline: 'publicAndExternal', - userId: id, - }) + store.commit('removeStatus', { timeline: 'friends', userId: id }) + store.commit('removeStatus', { timeline: 'public', userId: id }) + store.commit('removeStatus', { + timeline: 'publicAndExternal', + userId: id, }) + }) } const unblockUser = (store, id) => { - return store.rootState.api.backendInteractor - .unblockUser({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return apiUnblockUser({ id }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const removeUserFromFollowers = (store, id) => { - return store.rootState.api.backendInteractor - .removeUserFromFollowers({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return removeUserFromFollowers({ id }).then((relationship) => + store.commit('updateUserRelationship', [relationship]), + ) } const editUserNote = (store, { id, comment }) => { - return store.rootState.api.backendInteractor - .editUserNote({ id, comment }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return editUserNote({ id, comment }).then((relationship) => + store.commit('updateUserRelationship', [relationship]), + ) } const muteUser = (store, args) => { @@ -119,12 +136,14 @@ const muteUser = (store, args) => { store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addMuteId', id) - return store.rootState.api.backendInteractor - .muteUser({ id, expiresIn }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - store.commit('addMuteId', id) - }) + return apiMuteUser({ + id, + expiresIn, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => { + store.commit('updateUserRelationship', [relationship]) + store.commit('addMuteId', id) + }) } const unmuteUser = (store, id) => { @@ -132,39 +151,43 @@ const unmuteUser = (store, id) => { predictedRelationship.muting = false store.commit('updateUserRelationship', [predictedRelationship]) - return store.rootState.api.backendInteractor - .unmuteUser({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return apiUnmuteUser({ id }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const hideReblogs = (store, userId) => { - return store.rootState.api.backendInteractor - .followUser({ id: userId, reblogs: false }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - }) + return followUser({ + id: userId, + reblogs: false, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const showReblogs = (store, userId) => { - return store.rootState.api.backendInteractor - .followUser({ id: userId, reblogs: true }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return followUser({ + id: userId, + reblogs: true, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const muteDomain = (store, domain) => { - return store.rootState.api.backendInteractor - .muteDomain({ domain }) - .then(() => store.commit('addDomainMute', domain)) + return muteDomain({ + domain, + credentials: useOAuthStore().token, + }).then(() => store.commit('addDomainMute', domain)) } const unmuteDomain = (store, domain) => { - return store.rootState.api.backendInteractor - .unmuteDomain({ domain }) - .then(() => store.commit('removeDomainMute', domain)) + return unmuteDomain({ + domain, + credentials: useOAuthStore().token, + }).then(() => store.commit('removeDomainMute', domain)) } export const mutations = { @@ -385,55 +408,70 @@ const users = { }) }, fetchUser(store, id) { - return store.rootState.api.backendInteractor - .fetchUser({ id }) - .then((user) => { + return fetchUser({ + id, + credentials: useOAuthStore().token, + }) + .then(({ data: user }) => { store.commit('addNewUsers', [user]) return user }) + .catch((error) => { + if (error.statusCode === 404) { + console.warn(`User ${id} not found`) + } else { + throw error + } + }) }, fetchUserByName(store, name) { - return store.rootState.api.backendInteractor - .fetchUserByName({ name }) - .then((user) => { - store.commit('addNewUsers', [user]) - return user - }) + return fetchUserByName({ + name, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + store.commit('addNewUsers', [user]) + return user + }) }, fetchUserRelationship(store, id) { if (store.state.currentUser) { - store.rootState.api.backendInteractor - .fetchUserRelationship({ id }) - .then((relationships) => - store.commit('updateUserRelationship', relationships), - ) + fetchUserRelationship({ + id, + credentials: useOAuthStore().token, + }).then(({ data: relationships }) => + store.commit('updateUserRelationship', relationships), + ) } }, fetchUserInLists(store, id) { if (store.state.currentUser) { - store.rootState.api.backendInteractor - .fetchUserInLists({ id }) - .then((inLists) => store.commit('updateUserInLists', { id, inLists })) + fetchUserInLists({ + id, + credentials: useOAuthStore().token, + }).then(({ data: inLists }) => + store.commit('updateUserInLists', { id, inLists }), + ) } }, fetchBlocks(store, args) { const { reset } = args || {} const maxId = store.state.currentUser.blockIdsMaxId - return store.rootState.api.backendInteractor - .fetchBlocks({ maxId }) - .then((blocks) => { - if (reset) { - store.commit('saveBlockIds', map(blocks, 'id')) - } else { - map(blocks, 'id').map((id) => store.commit('addBlockId', id)) - } - if (blocks.length) { - store.commit('setBlockIdsMaxId', last(blocks).id) - } - store.commit('addNewUsers', blocks) - return blocks - }) + return fetchBlocks({ + maxId, + credentials: useOAuthStore().token, + }).then(({ data: blocks }) => { + if (reset) { + store.commit('saveBlockIds', map(blocks, 'id')) + } else { + map(blocks, 'id').map((id) => store.commit('addBlockId', id)) + } + if (blocks.length) { + store.commit('setBlockIdsMaxId', last(blocks).id) + } + store.commit('addNewUsers', blocks) + return blocks + }) }, blockUser(store, data) { return blockUser(store, data) @@ -457,20 +495,21 @@ const users = { const { reset } = args || {} const maxId = store.state.currentUser.muteIdsMaxId - return store.rootState.api.backendInteractor - .fetchMutes({ maxId }) - .then((mutes) => { - if (reset) { - store.commit('saveMuteIds', map(mutes, 'id')) - } else { - map(mutes, 'id').map((id) => store.commit('addMuteId', id)) - } - if (mutes.length) { - store.commit('setMuteIdsMaxId', last(mutes).id) - } - store.commit('addNewUsers', mutes) - return mutes - }) + return fetchMutes({ + maxId, + credentials: useOAuthStore().token, + }).then(({ data: mutes }) => { + if (reset) { + store.commit('saveMuteIds', map(mutes, 'id')) + } else { + map(mutes, 'id').map((id) => store.commit('addMuteId', id)) + } + if (mutes.length) { + store.commit('setMuteIdsMaxId', last(mutes).id) + } + store.commit('addNewUsers', mutes) + return mutes + }) }, muteUser(store, data) { return muteUser(store, data) @@ -491,12 +530,12 @@ const users = { return Promise.all(ids.map((d) => unmuteUser(store, d))) }, fetchDomainMutes(store) { - return store.rootState.api.backendInteractor - .fetchDomainMutes() - .then((domainMutes) => { - store.commit('saveDomainMutes', domainMutes) - return domainMutes - }) + return fetchDomainMutes({ + credentials: useOAuthStore().token, + }).then(({ data: domainMutes }) => { + store.commit('saveDomainMutes', domainMutes) + return domainMutes + }) }, muteDomain(store, domain) { return muteDomain(store, domain) @@ -513,24 +552,28 @@ const users = { fetchFriends({ rootState, commit }, id) { const user = rootState.users.usersObject[id] const maxId = last(user.friendIds) - return rootState.api.backendInteractor - .fetchFriends({ id, maxId }) - .then((friends) => { - commit('addNewUsers', friends) - commit('saveFriendIds', { id, friendIds: map(friends, 'id') }) - return friends - }) + return fetchFriends({ + id, + maxId, + credentials: useOAuthStore().token, + }).then(({ data: friends }) => { + commit('addNewUsers', friends) + commit('saveFriendIds', { id, friendIds: map(friends, 'id') }) + return friends + }) }, fetchFollowers({ rootState, commit }, id) { const user = rootState.users.usersObject[id] const maxId = last(user.followerIds) - return rootState.api.backendInteractor - .fetchFollowers({ id, maxId }) - .then((followers) => { - commit('addNewUsers', followers) - commit('saveFollowerIds', { id, followerIds: map(followers, 'id') }) - return followers - }) + return fetchFollowers({ + id, + maxId, + credentials: useOAuthStore().token, + }).then(({ data: followers }) => { + commit('addNewUsers', followers) + commit('saveFollowerIds', { id, followerIds: map(followers, 'id') }) + return followers + }) }, clearFriends({ commit }, userId) { commit('clearFriends', userId) @@ -539,18 +582,22 @@ const users = { commit('clearFollowers', userId) }, subscribeUser({ rootState, commit }, id) { - return rootState.api.backendInteractor - .followUser({ id, notify: true }) - .then((relationship) => - commit('updateUserRelationship', [relationship]), - ) + return followUser({ + id, + notify: true, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + commit('updateUserRelationship', [relationship]), + ) }, unsubscribeUser({ rootState, commit }, id) { - return rootState.api.backendInteractor - .followUser({ id, notify: false }) - .then((relationship) => - commit('updateUserRelationship', [relationship]), - ) + return followUser({ + id, + notify: false, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + commit('updateUserRelationship', [relationship]), + ) }, registerPushNotifications(store) { const token = store.state.currentUser.credentials @@ -611,12 +658,13 @@ const users = { }) }, searchUsers({ rootState, commit }, { query }) { - return rootState.api.backendInteractor - .searchUsers({ query }) - .then((users) => { - commit('addNewUsers', users) - return users - }) + return searchUsers({ + query, + credentials: useOAuthStore().token, + }).then(({ data: users }) => { + commit('addNewUsers', users) + return users + }) }, async signUp(store, userInfo) { const oauthStore = useOAuthStore() @@ -624,7 +672,7 @@ const users = { try { const token = await oauthStore.ensureAppToken() - const data = await apiService.register({ + const { data } = await register({ credentials: token, params: { ...userInfo }, }) @@ -645,8 +693,10 @@ const users = { throw e } }, - async getCaptcha(store) { - return store.rootState.api.backendInteractor.getCaptcha() + getCaptcha(store) { + return getCaptcha({ + credentials: useOAuthStore().token, + }).then(({ data }) => data) }, logout(store) { @@ -663,24 +713,21 @@ const users = { token: oauth.userToken, } - return oauthApi.revokeToken(params) + return revokeToken(params) }) .then(() => { store.commit('clearCurrentUser') store.dispatch('disconnectFromSocket') - oauth.clearToken() store.dispatch('stopFetchingTimeline', 'friends') - store.commit( - 'setBackendInteractor', - backendInteractorService(oauth.getToken), - ) store.dispatch('stopFetchingNotifications') - store.dispatch('stopFetchingLists') - store.dispatch('stopFetchingBookmarkFolders') + useListsStore().stopFetching() + useBookmarkFoldersStore().stopFetching() store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') store.dispatch('resetChats') + oauth.clearToken() + Cookies.remove('__Host-pleroma_key', { path: '/' }) useInterfaceStore().setLastTimeline('public-timeline') useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutHeight(windowHeight()) @@ -690,141 +737,137 @@ const users = { return new Promise((resolve, reject) => { const commit = store.commit const dispatch = store.dispatch + commit('beginLogin') - store.rootState.api.backendInteractor - .verifyCredentials(accessToken) - .then((data) => { - if (!data.error) { - const user = data - // user.credentials = userCredentials - user.credentials = accessToken - user.blockIds = [] - user.muteIds = [] - user.domainMutes = [] - commit('setCurrentUser', user) - useSyncConfigStore() - .initSyncConfig(user) - .then(() => { - useInterfaceStore() - .applyTheme() - .catch((e) => { - console.error('Error setting theme', e) - }) - }) - useUserHighlightStore().initUserHighlight(user) - commit('addNewUsers', [user]) + verifyCredentials({ + credentials: useOAuthStore().token, + }) + .then(({ data: user }) => { + // user.credentials = userCredentials + user.credentials = accessToken + user.blockIds = [] + user.muteIds = [] + user.domainMutes = [] + commit('setCurrentUser', user) - useEmojiStore().fetchEmoji() - - getNotificationPermission().then((permission) => - useInterfaceStore().setNotificationPermission(permission), - ) - - // Set our new backend interactor - commit( - 'setBackendInteractor', - backendInteractorService(accessToken), - ) - - // Do server-side storage migrations - - // Debug snippet to clean up storage and reset migrations - /* - // Reset wordfilter - Object.keys( - useSyncConfigStore().prefsStorage.simple.muteFilters - ).forEach(key => { - useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null }) + useSyncConfigStore() + .initSyncConfig(user) + .then(() => { + useInterfaceStore() + .applyTheme() + .catch((e) => { + console.error('Error setting theme', e) + }) }) + useUserHighlightStore().initUserHighlight(user) + commit('addNewUsers', [user]) - // Reset flag to 0 to re-run migrations - useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) - /**/ + useEmojiStore().fetchEmoji() - if (user.token) { - dispatch('setWsToken', user.token) + getNotificationPermission().then((permission) => + useInterfaceStore().setNotificationPermission(permission), + ) - // Initialize the shout socket. - dispatch('initializeSocket') - } + // Do server-side storage migrations - const startPolling = () => { - // Start getting fresh posts. - dispatch('startFetchingTimeline', { timeline: 'friends' }) + // Debug snippet to clean up storage and reset migrations + /* + // Reset wordfilter + Object.keys( + useSyncConfigStore().prefsStorage.simple.muteFilters + ).forEach(key => { + useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null }) + }) - // Start fetching notifications - dispatch('startFetchingNotifications') + // Reset flag to 0 to re-run migrations + useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) + /**/ - if ( - useInstanceCapabilitiesStore().pleromaChatMessagesAvailable - ) { - // Start fetching chats - dispatch('startFetchingChats') - } - } + if (user.token) { + dispatch('setWsToken', user.token) - dispatch('startFetchingLists') - dispatch('startFetchingBookmarkFolders') + // Initialize the shout socket. + dispatch('initializeSocket') + } - if (user.locked) { - dispatch('startFetchingFollowRequests') - } + const startPolling = () => { + // Start getting fresh posts. + dispatch('startFetchingTimeline', { timeline: 'friends' }) - if (useMergedConfigStore().mergedConfig.useStreamingApi) { - dispatch('fetchTimeline', { timeline: 'friends', since: null }) - dispatch('fetchNotifications', { since: null }) - dispatch('enableMastoSockets', true) - .catch((error) => { - console.error( - 'Failed initializing MastoAPI Streaming socket', - error, - ) - }) - .then(() => { - dispatch('fetchChats', { latest: true }) - setTimeout( - () => dispatch('setNotificationsSilence', false), - 10000, - ) - }) - } else { - startPolling() - } + // Start fetching notifications + dispatch('startFetchingNotifications') - // Get user mutes - dispatch('fetchMutes') - - useInterfaceStore().setLayoutWidth(windowWidth()) - useInterfaceStore().setLayoutHeight(windowHeight()) - - // Fetch our friends - store.rootState.api.backendInteractor - .fetchFriends({ id: user.id }) - .then((friends) => commit('addNewUsers', friends)) - } else { - const response = data.error - // Authentication failed - commit('endLogin') - - // remove authentication token on client/authentication errors - if ([400, 401, 403, 422].includes(response.status)) { - useOAuthStore().clearToken() - } - - if (response.status === 401) { - reject(new Error('Wrong username or password')) - } else { - reject(new Error('An error occurred, please try again')) + if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { + // Start fetching chats + dispatch('startFetchingChats') } } + + useListsStore().startFetching() + useBookmarkFoldersStore().startFetching() + + if (user.locked) { + dispatch('startFetchingFollowRequests') + } + + if (useMergedConfigStore().mergedConfig.useStreamingApi) { + dispatch('fetchTimeline', { + timeline: 'friends', + sinceId: null, + }) + dispatch('fetchNotifications', { sinceId: null }) + dispatch('enableMastoSockets', true) + .catch((error) => { + console.error( + 'Failed initializing MastoAPI Streaming socket', + error, + ) + }) + .then(() => { + dispatch('fetchChats', { latest: true }) + setTimeout( + () => dispatch('setNotificationsSilence', false), + 10000, + ) + }) + } else { + startPolling() + } + + // Start fetching things that don't need to block the UI + useAnnouncementsStore().startFetchingAnnouncements() + + dispatch('fetchMutes') + dispatch('loadDrafts') + + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) + + // Fetch our friends + fetchFriends({ id: user.id }).then(({ data: friends }) => + commit('addNewUsers', friends), + ) commit('endLogin') resolve() }) .catch((error) => { console.error(error) + + // Authentication failed commit('endLogin') - reject(new Error('Failed to connect to server, try again')) + + // remove authentication token on client/authentication errors + if ([400, 401, 403, 422].includes(error.statusCode)) { + useOAuthStore().clearToken() + } + + commit('endLogin') + if (error.tatusCode === 401) { + throw new Error('Wrong username or password', error) + } else { + throw new Error('An error occurred, please try again', error) + } }) }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js deleted file mode 100644 index 5ab1fa18e..000000000 --- a/src/services/api/api.service.js +++ /dev/null @@ -1,2527 +0,0 @@ -import { concat, each, last, map } from 'lodash' - -import { - parseAttachment, - parseChat, - parseLinkHeaderPagination, - parseNotification, - parseSource, - parseStatus, - parseUser, -} from '../entity_normalizer/entity_normalizer.service.js' -import { RegistrationError, StatusCodeError } from '../errors/errors' - -/* eslint-env browser */ -const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' -const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' -const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' -const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' -const CHANGE_EMAIL_URL = '/api/pleroma/change_email' -const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' -const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' -const ALIASES_URL = '/api/pleroma/aliases' -const SUGGESTIONS_URL = '/api/v1/suggestions' -const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' -const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' - -const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' -const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' - -const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' -const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' -const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' - -const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' -const MASTODON_REGISTRATION_URL = '/api/v1/accounts' -const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' -const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' -const MASTODON_DISMISS_NOTIFICATION_URL = (id) => - `/api/v1/notifications/${id}/dismiss` -const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` -const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` -const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` -const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` -const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` -const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` -const MASTODON_FOLLOWING_URL = (id) => `/api/v1/accounts/${id}/following` -const MASTODON_FOLLOWERS_URL = (id) => `/api/v1/accounts/${id}/followers` -const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' -const MASTODON_APPROVE_USER_URL = (id) => - `/api/v1/follow_requests/${id}/authorize` -const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` -const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct' -const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' -const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' -const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` -const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` -const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` -const MASTODON_USER_URL = '/api/v1/accounts' -const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup' -const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' -const MASTODON_USER_TIMELINE_URL = (id) => `/api/v1/accounts/${id}/statuses` -const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` -const MASTODON_LIST_URL = (id) => `/api/v1/lists/${id}` -const MASTODON_LIST_TIMELINE_URL = (id) => `/api/v1/timelines/list/${id}` -const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` -const MASTODON_TAG_TIMELINE_URL = (tag) => `/api/v1/timelines/tag/${tag}` -const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks' -const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble' -const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' -const MASTODON_USER_MUTES_URL = '/api/v1/mutes/' -const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` -const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` -const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` -const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` -const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => - `/api/v1/accounts/${id}/remove_from_followers` -const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` -const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` -const MASTODON_UNBOOKMARK_STATUS_URL = (id) => - `/api/v1/statuses/${id}/unbookmark` -const MASTODON_POST_STATUS_URL = '/api/v1/statuses' -const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' -const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` -const MASTODON_POLL_URL = (id) => `/api/v1/polls/${id}` -const MASTODON_STATUS_FAVORITEDBY_URL = (id) => - `/api/v1/statuses/${id}/favourited_by` -const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => - `/api/v1/statuses/${id}/reblogged_by` -const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' -const MASTODON_REPORT_USER_URL = '/api/v1/reports' -const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` -const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` -const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` -const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` -const MASTODON_SEARCH_2 = '/api/v2/search' -const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' -const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' -const MASTODON_LISTS_URL = '/api/v1/lists' -const MASTODON_STREAMING = '/api/v1/streaming' -const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' -const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' -const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => - `/api/v1/announcements/${id}/dismiss` -const PLEROMA_EMOJI_REACTIONS_URL = (id) => - `/api/v1/pleroma/statuses/${id}/reactions` -const PLEROMA_EMOJI_REACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' -const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` -const PLEROMA_CHAT_MESSAGES_URL = (id) => `/api/v1/pleroma/chats/${id}/messages` -const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` -const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => - `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` -const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' -const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' -const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' -const PLEROMA_EDIT_ANNOUNCEMENT_URL = (id) => - `/api/v1/pleroma/admin/announcements/${id}` -const PLEROMA_DELETE_ANNOUNCEMENT_URL = (id) => - `/api/v1/pleroma/admin/announcements/${id}` -const PLEROMA_SCROBBLES_URL = (id) => `/api/v1/pleroma/accounts/${id}/scrobbles` -const PLEROMA_STATUS_QUOTES_URL = (id) => - `/api/v1/pleroma/statuses/${id}/quotes` -const PLEROMA_USER_FAVORITES_TIMELINE_URL = (id) => - `/api/v1/pleroma/accounts/${id}/favourites` -const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' -const PLEROMA_BOOKMARK_FOLDER_URL = (id) => - `/api/v1/pleroma/bookmark_folders/${id}` - -const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports' -const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config' -const PLEROMA_ADMIN_DESCRIPTIONS_URL = - '/api/v1/pleroma/admin/config/descriptions' -const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends' -const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = - '/api/v1/pleroma/admin/frontends/install' - -const PLEROMA_ADMIN_USERS_URL = '/api/v1/pleroma/admin/users' -const PLEROMA_ADMIN_USERS_URL_SHOW = (nickname) => - `/api/v1/pleroma/admin/users/${nickname}` -const PLEROMA_ADMIN_USERS_URL_LIST = ({ - page, - pageSize, - filters = {}, - query = '', - name = '', - email = '', -}) => { - const { - local = false, - external = false, - active = false, - needApproval = false, - unconfirmed = false, - deactivated = false, - isAdmin = true, - isModerator = true, - } = filters - const filters_str = [ - local && 'local', - external && 'external', - active && 'active', - needApproval && 'need_approval', - unconfirmed && 'unconfirmed', - deactivated && 'deactivated', - isAdmin && 'is_admin', - isModerator && 'is_moderator', - ] - .filter((x) => x) - .join(',') - return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}` -} -const PLEROMA_ADMIN_TAG_USER_URL = '/api/pleroma/admin/users/tag' -const PLEROMA_ADMIN_PERMISSION_GROUP_URL = (right) => - `/api/pleroma/admin/users/permission_group/${right}` -const PLEROMA_ADMIN_ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate' -const PLEROMA_ADMIN_DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate' -const PLEROMA_ADMIN_SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest' -const PLEROMA_ADMIN_UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest' -const PLEROMA_ADMIN_APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve' -const PLEROMA_ADMIN_CONFIRM_USERS_URL = - '/api/v1/pleroma/admin/users/confirm_email' -const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL = - '/api/v1/pleroma/admin/users/resend_confirmation_email' -const PLEROMA_ADMIN_LIST_STATUSES_URL = ({ - id, - page, - pageSize, - godmode, - withReblogs, -}) => - `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}` -const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) => - `/api/v1/pleroma/admin/statuses/${id}` -const PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL = - '/api/v1/pleroma/admin/users/force_password_reset' -const PLEROMA_ADMIN_DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa' -const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' -const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' -const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => - `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` -const PLEROMA_EMOJI_PACK_URL = (name) => - `/api/v1/pleroma/emoji/pack?name=${name}` -const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' -const PLEROMA_EMOJI_PACKS_DL_REMOTE_ZIP_URL = - '/api/v1/pleroma/emoji/packs/download_zip' -const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) => - `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` -const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => - `/api/v1/pleroma/emoji/packs/files?name=${name}` - -const oldfetch = window.fetch - -const fetch = (url, options) => { - options = options || {} - const baseUrl = '' - const fullUrl = baseUrl + url - options.credentials = 'same-origin' - return oldfetch(fullUrl, options) -} - -const promisedRequest = ({ - method, - url, - params, - payload, - credentials, - headers = {}, -}) => { - const options = { - method, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...headers, - }, - } - if (params) { - url += - '?' + - Object.entries(params) - .map( - ([key, value]) => - encodeURIComponent(key) + '=' + encodeURIComponent(value), - ) - .join('&') - } - if (payload) { - options.body = JSON.stringify(payload) - } - if (credentials) { - options.headers = { - ...options.headers, - ...authHeaders(credentials), - } - } - return fetch(url, options).then((response) => { - return new Promise((resolve, reject) => { - // 204 is "No content", which fails to parse json (as you'd might think) - if (response.ok && response.status === 204) resolve() - - return response - .json() - .then((json) => { - if (!response.ok) { - return reject( - new StatusCodeError( - response.status, - json, - { url, options }, - response, - ), - ) - } - return resolve(json) - }) - .catch((error) => { - return reject( - new StatusCodeError( - response.status, - error, - { url, options }, - response, - ), - ) - }) - }) - }) -} - -const updateNotificationSettings = ({ credentials, settings }) => { - const form = new FormData() - - each(settings, (value, key) => { - form.append(key, value) - }) - - return fetch( - `${NOTIFICATION_SETTINGS_URL}?${new URLSearchParams(settings)}`, - { - headers: authHeaders(credentials), - method: 'PUT', - body: form, - }, - ).then((data) => data.json()) -} - -const updateProfileImages = ({ - credentials, - avatar = null, - avatarName = null, - banner = null, - background = null, -}) => { - const form = new FormData() - if (avatar !== null) { - if (avatarName !== null) { - form.append('avatar', avatar, avatarName) - } else { - form.append('avatar', avatar) - } - } - if (banner !== null) form.append('header', banner) - if (background !== null) form.append('pleroma_background_image', background) - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: form, - }) - .then((data) => data.json()) - .then((data) => { - if (data.error) { - throw new Error(data.error) - } - return parseUser(data) - }) -} - -const updateProfile = ({ credentials, params }) => { - const formData = new FormData() - - for (const name in params) { - if (name === 'fields_attributes') { - params[name].forEach((param, i) => { - formData.append(name + `[${i}][name]`, param.name) - formData.append(name + `[${i}][value]`, param.value) - }) - } else { - if (typeof params[name] === 'object') { - console.warn( - 'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.', - ) - console.warn('Object:\n' + JSON.stringify(params[name], null, 2)) - } - formData.append(name, params[name]) - } - } - - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: formData, - }) - .then((data) => data.json()) - .then((data) => parseUser(data)) -} - -const updateProfileJSON = ({ credentials, params }) => { - return promisedRequest({ - url: MASTODON_PROFILE_UPDATE_URL, - credentials, - payload: params, - method: 'PATCH', - }).then((data) => parseUser(data)) -} - -// Params needed: -// nickname -// email -// fullname -// password -// password_confirm -// -// Optional -// bio -// homepage -// location -// token -// language -const register = ({ params, credentials }) => { - const { nickname, ...rest } = params - return fetch(MASTODON_REGISTRATION_URL, { - method: 'POST', - headers: { - ...authHeaders(credentials), - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nickname, - locale: 'en_US', - agreement: true, - ...rest, - }), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return response.json().then((error) => { - throw new RegistrationError(error) - }) - } - }) -} - -const getCaptcha = () => - fetch('/api/pleroma/captcha').then((resp) => resp.json()) - -const authHeaders = (accessToken) => { - if (accessToken) { - return { Authorization: `Bearer ${accessToken}` } - } else { - return {} - } -} - -const followUser = ({ id, credentials, ...options }) => { - const url = MASTODON_FOLLOW_URL(id) - const form = {} - if (options.reblogs !== undefined) { - form.reblogs = options.reblogs - } - if (options.notify !== undefined) { - form.notify = options.notify - } - return fetch(url, { - body: JSON.stringify(form), - headers: { - ...authHeaders(credentials), - 'Content-Type': 'application/json', - }, - method: 'POST', - }).then((data) => data.json()) -} - -const unfollowUser = ({ id, credentials }) => { - const url = MASTODON_UNFOLLOW_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const fetchUserInLists = ({ id, credentials }) => { - const url = MASTODON_USER_IN_LISTS(id) - return fetch(url, { - headers: authHeaders(credentials), - }).then((data) => data.json()) -} - -const pinOwnStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_PIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const unpinOwnStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNPIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const muteConversation = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_MUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const unmuteConversation = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNMUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const blockUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.duration = expiresIn - } - - return promisedRequest({ - url: MASTODON_BLOCK_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -const unblockUser = ({ id, credentials }) => { - return fetch(MASTODON_UNBLOCK_USER_URL(id), { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const removeUserFromFollowers = ({ id, credentials }) => { - return fetch(MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const editUserNote = ({ id, credentials, comment }) => { - return promisedRequest({ - url: MASTODON_USER_NOTE_URL(id), - credentials, - payload: { - comment, - }, - method: 'POST', - }) -} - -const approveUser = ({ id, credentials }) => { - const url = MASTODON_APPROVE_USER_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const denyUser = ({ id, credentials }) => { - const url = MASTODON_DENY_USER_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const fetchUser = ({ id, credentials }) => { - const url = `${MASTODON_USER_URL}/${id}` - return promisedRequest({ url, credentials }).then((data) => parseUser(data)) -} - -const fetchUserByName = ({ name, credentials }) => { - return promisedRequest({ - url: MASTODON_USER_LOOKUP_URL, - credentials, - params: { acct: name }, - }) - .then((data) => data.id) - .catch((error) => { - if (error && error.statusCode === 404) { - // Either the backend does not support lookup endpoint, - // or there is no user with such name. Fallback and treat name as id. - return name - } else { - throw error - } - }) - .then((id) => fetchUser({ id, credentials })) -} - -const fetchUserRelationship = ({ id, credentials }) => { - const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}` - return fetch(url, { headers: authHeaders(credentials) }).then((response) => { - return new Promise((resolve, reject) => - response.json().then((json) => { - if (!response.ok) { - return reject( - new StatusCodeError(response.status, json, { url }, response), - ) - } - return resolve(json) - }), - ) - }) -} - -const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => { - let url = MASTODON_FOLLOWING_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - 'with_relationships=true', - ] - .filter((_) => _) - .join('&') - - url = url + (args ? '?' + args : '') - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const exportFriends = ({ id, credentials }) => { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this - return new Promise(async (resolve, reject) => { - try { - let friends = [] - let more = true - while (more) { - const maxId = friends.length > 0 ? last(friends).id : undefined - const users = await fetchFriends({ id, maxId, credentials }) - friends = concat(friends, users) - if (users.length === 0) { - more = false - } - } - resolve(friends) - } catch (err) { - reject(err) - } - }) -} - -const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => { - let url = MASTODON_FOLLOWERS_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - 'with_relationships=true', - ] - .filter((_) => _) - .join('&') - - url += args ? '?' + args : '' - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const fetchFollowRequests = ({ credentials }) => { - const url = MASTODON_FOLLOW_REQUESTS_URL - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const fetchLists = ({ credentials }) => { - const url = MASTODON_LISTS_URL - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const createList = ({ title, credentials }) => { - const url = MASTODON_LISTS_URL - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ title }), - }).then((data) => data.json()) -} - -const getList = ({ listId, credentials }) => { - const url = MASTODON_LIST_URL(listId) - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const updateList = ({ listId, title, credentials }) => { - const url = MASTODON_LIST_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'PUT', - body: JSON.stringify({ title }), - }) -} - -const getListAccounts = ({ listId, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(({ id }) => id)) -} - -const addAccountsToList = ({ listId, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ account_ids: accountIds }), - }) -} - -const removeAccountsFromList = ({ listId, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'DELETE', - body: JSON.stringify({ account_ids: accountIds }), - }) -} - -const deleteList = ({ listId, credentials }) => { - const url = MASTODON_LIST_URL(listId) - return fetch(url, { - method: 'DELETE', - headers: authHeaders(credentials), - }) -} - -const fetchConversation = ({ id, credentials }) => { - const urlContext = MASTODON_STATUS_CONTEXT_URL(id) - return fetch(urlContext, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching timeline', data) - }) - .then((data) => data.json()) - .then(({ ancestors, descendants }) => ({ - ancestors: ancestors.map(parseStatus), - descendants: descendants.map(parseStatus), - })) -} - -const fetchStatus = ({ id, credentials }) => { - const url = MASTODON_STATUS_URL(id) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching timeline', { cause: data }) - }) - .then((data) => data.json()) - .then((data) => parseStatus(data)) -} - -const fetchStatusSource = ({ id, credentials }) => { - const url = MASTODON_STATUS_SOURCE_URL(id) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching source', { cause: data }) - }) - .then((data) => data.json()) - .then((data) => parseSource(data)) -} - -const fetchStatusHistory = ({ status, credentials }) => { - const url = MASTODON_STATUS_HISTORY_URL(status.id) - return promisedRequest({ url, credentials }).then((data) => { - data.reverse() - return data.map((item) => { - item.originalStatus = status - return parseStatus(item) - }) - }) -} - -const adminSetUsersTags = ({ - tags, - credentials, - value, - screen_names: nicknames, -}) => { - return promisedRequest({ - url: PLEROMA_ADMIN_TAG_USER_URL, - method: value ? 'PUT' : 'DELETE', - credentials, - payload: { - nicknames, - tags, - }, - }) -} - -const adminSetUsersRight = ({ - right, - credentials, - value, - screen_names: nicknames, -}) => { - return promisedRequest({ - url: PLEROMA_ADMIN_PERMISSION_GROUP_URL(right), - method: value ? 'POST' : 'DELETE', - credentials, - payload: { - nicknames, - }, - }) -} - -const adminSetUsersActivationStatus = ({ - credentials, - screen_names: nicknames, - value, -}) => { - return promisedRequest({ - url: value - ? PLEROMA_ADMIN_ACTIVATE_USERS_URL - : PLEROMA_ADMIN_DEACTIVATE_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) -} - -const adminSetUsersApprovalStatus = ({ - credentials, - screen_names: nicknames, -}) => { - return promisedRequest({ - url: PLEROMA_ADMIN_APPROVE_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) -} - -const adminSetUsersConfirmationStatus = ({ - credentials, - screen_names: nicknames, -}) => { - return promisedRequest({ - url: PLEROMA_ADMIN_CONFIRM_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) -} - -const adminSetUsersSuggestionStatus = ({ - credentials, - screen_names: nicknames, - value, -}) => { - return promisedRequest({ - url: value - ? PLEROMA_ADMIN_SUGGEST_USERS_URL - : PLEROMA_ADMIN_UNSUGGEST_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) -} - -const adminGetUserData = ({ credentials, screen_name: nickname }) => { - return promisedRequest({ - url: PLEROMA_ADMIN_USERS_URL_SHOW(nickname), - method: 'GET', - credentials, - }) -} - -const adminDeleteAccounts = ({ credentials, screen_names: nicknames }) => { - return promisedRequest({ - url: PLEROMA_ADMIN_USERS_URL, - method: 'DELETE', - credentials, - payload: { - nicknames, - }, - }) -} - -const fetchTimeline = ({ - timeline, - credentials, - since = false, - minId = false, - until = false, - userId = false, - listId = false, - statusId = false, - tag = false, - withMuted = false, - replyVisibility = 'all', - includeTypes = [], - bookmarkFolderId = false, -}) => { - const timelineUrls = { - public: MASTODON_PUBLIC_TIMELINE, - friends: MASTODON_USER_HOME_TIMELINE_URL, - dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, - notifications: MASTODON_USER_NOTIFICATIONS_URL, - publicAndExternal: MASTODON_PUBLIC_TIMELINE, - user: MASTODON_USER_TIMELINE_URL, - media: MASTODON_USER_TIMELINE_URL, - list: MASTODON_LIST_TIMELINE_URL, - favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, - publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, - tag: MASTODON_TAG_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, - quotes: PLEROMA_STATUS_QUOTES_URL, - bubble: AKKOMA_BUBBLE_TIMELINE_URL, - } - const isNotifications = timeline === 'notifications' - const params = [] - - let url = timelineUrls[timeline] - - if (timeline === 'favorites' && userId) { - url = timelineUrls.publicFavorites(userId) - } - - if (timeline === 'user' || timeline === 'media') { - url = url(userId) - } - - if (timeline === 'list') { - url = url(listId) - } - - if (timeline === 'quotes') { - url = url(statusId) - } - - if (minId) { - params.push(['min_id', minId]) - } - if (since) { - params.push(['since_id', since]) - } - if (until) { - params.push(['max_id', until]) - } - if (tag) { - url = url(tag) - } - if (timeline === 'media') { - params.push(['only_media', 1]) - } - if (timeline === 'public') { - params.push(['local', true]) - } - if (timeline === 'public' || timeline === 'publicAndExternal') { - params.push(['only_media', false]) - } - if (timeline !== 'favorites' && timeline !== 'bookmarks') { - params.push(['with_muted', withMuted]) - } - if (replyVisibility !== 'all') { - params.push(['reply_visibility', replyVisibility]) - } - if (includeTypes.size > 0) { - includeTypes.forEach((type) => { - params.push(['include_types[]', type]) - }) - } - if (timeline === 'bookmarks' && bookmarkFolderId) { - params.push(['folder_id', bookmarkFolderId]) - } - - params.push(['limit', 20]) - - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - - return fetch(url, { headers: authHeaders(credentials) }).then( - async (response) => { - const success = response.ok - - const data = await response.json() - - if (success && !data.errors) { - const pagination = parseLinkHeaderPagination( - response.headers.get('Link'), - { - flakeId: timeline !== 'bookmarks' && timeline !== 'notifications', - }, - ) - - return { - data: data.map(isNotifications ? parseNotification : parseStatus), - pagination, - } - } else { - data.errors ||= [] - data.status = response.status - data.statusText = response.statusText - return data - } - }, - ) -} - -const fetchPinnedStatuses = ({ id, credentials }) => { - const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true' - return promisedRequest({ url, credentials }).then((data) => - data.map(parseStatus), - ) -} - -const verifyCredentials = (user) => { - return fetch(MASTODON_LOGIN_URL, { - headers: authHeaders(user), - }) - .then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) - .then((data) => (data.error ? data : parseUser(data))) -} - -const favorite = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_FAVORITE_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const unfavorite = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNFAVORITE_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const retweet = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_RETWEET_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const unretweet = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNRETWEET_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const bookmarkStatus = ({ id, credentials, ...options }) => { - return promisedRequest({ - url: MASTODON_BOOKMARK_STATUS_URL(id), - headers: authHeaders(credentials), - method: 'POST', - payload: { - folder_id: options.folder_id, - }, - }) -} - -const unbookmarkStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNBOOKMARK_STATUS_URL(id), - headers: authHeaders(credentials), - method: 'POST', - }) -} - -const postStatus = ({ - credentials, - status, - spoilerText, - visibility, - sensitive, - poll, - mediaIds = [], - inReplyToStatusId, - quoteId, - contentType, - preview, - idempotencyKey, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - form.append('source', 'Pleroma FE') - if (spoilerText) form.append('spoiler_text', spoilerText) - if (visibility) form.append('visibility', visibility) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - if (inReplyToStatusId) { - form.append('in_reply_to_id', inReplyToStatusId) - } - if (quoteId) { - form.append('quote_id', quoteId) - } - if (preview) { - form.append('preview', 'true') - } - - const postHeaders = authHeaders(credentials) - if (idempotencyKey) { - postHeaders['idempotency-key'] = idempotencyKey - } - - return fetch(MASTODON_POST_STATUS_URL, { - body: form, - method: 'POST', - headers: postHeaders, - }) - .then((response) => { - return response.json() - }) - .then((data) => (data.error ? data : parseStatus(data))) -} - -const editStatus = ({ - id, - credentials, - status, - spoilerText, - sensitive, - poll, - mediaIds = [], - contentType, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - if (spoilerText) form.append('spoiler_text', spoilerText) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - - const putHeaders = authHeaders(credentials) - - return fetch(MASTODON_STATUS_URL(id), { - body: form, - method: 'PUT', - headers: putHeaders, - }) - .then((response) => { - return response.json() - }) - .then((data) => (data.error ? data : parseStatus(data))) -} - -const deleteStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_DELETE_URL(id), - credentials, - method: 'DELETE', - }) -} - -const uploadMedia = ({ formData, credentials }) => { - return fetch(MASTODON_MEDIA_UPLOAD_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }) - .then((data) => data.json()) - .then((data) => parseAttachment(data)) -} - -const setMediaDescription = ({ id, description, credentials }) => { - return promisedRequest({ - url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, - method: 'PUT', - headers: authHeaders(credentials), - payload: { - description, - }, - }).then((data) => parseAttachment(data)) -} - -const importMutes = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(MUTES_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const importBlocks = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(BLOCKS_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const importFollows = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(FOLLOW_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const deleteAccount = ({ credentials, password }) => { - const form = new FormData() - - form.append('password', password) - - return fetch(DELETE_ACCOUNT_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const changeEmail = ({ credentials, email, password }) => { - const form = new FormData() - - form.append('email', email) - form.append('password', password) - - return fetch(CHANGE_EMAIL_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const moveAccount = ({ credentials, password, targetAccount }) => { - const form = new FormData() - - form.append('password', password) - form.append('target_account', targetAccount) - - return fetch(MOVE_ACCOUNT_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const addAlias = ({ credentials, alias }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'PUT', - credentials, - payload: { alias }, - }) -} - -const deleteAlias = ({ credentials, alias }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'DELETE', - credentials, - payload: { alias }, - }) -} - -const listAliases = ({ credentials }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) -} - -const changePassword = ({ - credentials, - password, - newPassword, - newPasswordConfirmation, -}) => { - const form = new FormData() - - form.append('password', password) - form.append('new_password', newPassword) - form.append('new_password_confirmation', newPasswordConfirmation) - - return fetch(CHANGE_PASSWORD_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const settingsMFA = ({ credentials }) => { - return fetch(MFA_SETTINGS_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} - -const mfaDisableOTP = ({ credentials, password }) => { - const form = new FormData() - - form.append('password', password) - - return fetch(MFA_DISABLE_OTP_URL, { - body: form, - method: 'DELETE', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const mfaConfirmOTP = ({ credentials, password, token }) => { - const form = new FormData() - - form.append('password', password) - form.append('code', token) - - return fetch(MFA_CONFIRM_OTP_URL, { - body: form, - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} -const mfaSetupOTP = ({ credentials }) => { - return fetch(MFA_SETUP_OTP_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} -const generateMfaBackupCodes = ({ credentials }) => { - return fetch(MFA_BACKUP_CODES_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} - -const fetchMutes = ({ maxId, credentials }) => { - const query = new URLSearchParams({ with_relationships: true }) - if (maxId) { - query.append('max_id', maxId) - } - return promisedRequest({ - url: `${MASTODON_USER_MUTES_URL}?${query.toString()}`, - credentials, - }).then((users) => users.map(parseUser)) -} - -const muteUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.expires_in = expiresIn - } - - return promisedRequest({ - url: MASTODON_MUTE_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -const unmuteUser = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNMUTE_USER_URL(id), - credentials, - method: 'POST', - }) -} - -const fetchBlocks = ({ maxId, credentials }) => { - const query = new URLSearchParams({ with_relationships: true }) - if (maxId) { - query.append('max_id', maxId) - } - return promisedRequest({ - url: `${MASTODON_USER_BLOCKS_URL}?${query.toString()}`, - credentials, - }).then((users) => users.map(parseUser)) -} - -const addBackup = ({ credentials }) => { - return promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'POST', - credentials, - }) -} - -const listBackups = ({ credentials }) => { - return promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) -} - -const fetchOAuthTokens = ({ credentials }) => { - const url = '/api/oauth_tokens.json' - - return fetch(url, { - headers: authHeaders(credentials), - }).then((data) => { - if (data.ok) { - return data.json() - } - throw new Error('Error fetching auth tokens', data) - }) -} - -const revokeOAuthToken = ({ id, credentials }) => { - const url = `/api/oauth_tokens/${id}` - - return fetch(url, { - headers: authHeaders(credentials), - method: 'DELETE', - }) -} - -const suggestions = ({ credentials }) => { - return fetch(SUGGESTIONS_URL, { - headers: authHeaders(credentials), - }).then((data) => data.json()) -} - -const markNotificationsAsSeen = ({ id, credentials, single = false }) => { - const body = new FormData() - - if (single) { - body.append('id', id) - } else { - body.append('max_id', id) - } - - return fetch(NOTIFICATION_READ_URL, { - body, - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const vote = ({ pollId, choices, credentials }) => { - const form = new FormData() - form.append('choices', choices) - - return promisedRequest({ - url: MASTODON_VOTE_URL(encodeURIComponent(pollId)), - method: 'POST', - credentials, - payload: { - choices, - }, - }) -} - -const fetchPoll = ({ pollId, credentials }) => { - return promisedRequest({ - url: MASTODON_POLL_URL(encodeURIComponent(pollId)), - method: 'GET', - credentials, - }) -} - -const fetchFavoritedByUsers = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_STATUS_FAVORITEDBY_URL(id), - method: 'GET', - credentials, - }).then((users) => users.map(parseUser)) -} - -const fetchRebloggedByUsers = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_STATUS_REBLOGGEDBY_URL(id), - method: 'GET', - credentials, - }).then((users) => users.map(parseUser)) -} - -const fetchEmojiReactions = ({ id, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_REACTIONS_URL(id), - credentials, - }).then((reactions) => - reactions.map((r) => { - r.accounts = r.accounts.map(parseUser) - return r - }), - ) -} - -const reactWithEmoji = ({ id, emoji, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_REACT_URL(id, emoji), - method: 'PUT', - credentials, - }).then(parseStatus) -} - -const unreactWithEmoji = ({ id, emoji, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), - method: 'DELETE', - credentials, - }).then(parseStatus) -} - -const reportUser = ({ credentials, userId, statusIds, comment, forward }) => { - return promisedRequest({ - url: MASTODON_REPORT_USER_URL, - method: 'POST', - payload: { - account_id: userId, - status_ids: statusIds, - comment, - forward, - }, - credentials, - }) -} - -const searchUsers = ({ credentials, query }) => { - return promisedRequest({ - url: MASTODON_USER_SEARCH_URL, - params: { - q: query, - resolve: true, - }, - credentials, - }).then((data) => data.map(parseUser)) -} - -const search2 = ({ - credentials, - q, - resolve, - limit, - offset, - following, - type, -}) => { - let url = MASTODON_SEARCH_2 - const params = [] - - if (q) { - params.push(['q', encodeURIComponent(q)]) - } - - if (resolve) { - params.push(['resolve', resolve]) - } - - if (limit) { - params.push(['limit', limit]) - } - - if (offset) { - params.push(['offset', offset]) - } - - if (following) { - params.push(['following', true]) - } - - if (type) { - params.push(['type', type]) - } - - params.push(['with_relationships', true]) - - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching search result', data) - }) - .then((data) => { - return data.json() - }) - .then((data) => { - data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u)) - data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s)) - return data - }) -} - -const fetchKnownDomains = ({ credentials }) => { - return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) -} - -const fetchDomainMutes = ({ credentials }) => { - return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) -} - -const muteDomain = ({ domain, credentials }) => { - return promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'POST', - payload: { domain }, - credentials, - }) -} - -const unmuteDomain = ({ domain, credentials }) => { - return promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'DELETE', - payload: { domain }, - credentials, - }) -} - -const dismissNotification = ({ credentials, id }) => { - return promisedRequest({ - url: MASTODON_DISMISS_NOTIFICATION_URL(id), - method: 'POST', - payload: { id }, - credentials, - }) -} - -const adminFetchAnnouncements = ({ credentials }) => { - return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials }) -} - -const fetchAnnouncements = ({ credentials }) => { - return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials }) -} - -const dismissAnnouncement = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id), - credentials, - method: 'POST', - }) -} - -const adminListUsers = ({ opts, credentials }) => { - // the reported list is hardly useful because standards are for dating i guess, - // so make sure to fetchIfMissing right afterward using this call - const url = PLEROMA_ADMIN_USERS_URL_LIST(opts) - - return promisedRequest({ - url, - credentials, - method: 'GET', - }) -} - -const adminResendConfirmationEmail = ({ - screen_names: nicknames, - credentials, -}) => { - const url = PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL - return promisedRequest({ - url, - credentials, - method: 'PATCH', - payload: { - nicknames, - }, - }) -} - -const adminRequirePasswordChange = ({ - screen_names: nicknames, - credentials, -}) => { - const url = PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL - return promisedRequest({ - url, - credentials, - method: 'PATCH', - payload: { - nicknames, - }, - }) -} - -const adminDisableMFA = ({ screen_name: nickname, credentials }) => { - const url = PLEROMA_ADMIN_DISABLE_MFA_URL - return promisedRequest({ - url, - credentials, - method: 'PUT', - payload: { - nickname, - }, - }) -} - -const adminListStatuses = ({ opts, credentials }) => { - const url = PLEROMA_ADMIN_LIST_STATUSES_URL(opts) - - return promisedRequest({ - url, - credentials, - method: 'GET', - }) -} - -const adminChangeStatusScope = ({ - opts: { id, sensitive, visibility }, - credentials, -}) => { - const url = PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL(id) - var payload = {} - if (typeof sensitive !== 'undefined') { - payload['sensitive'] = sensitive - } - if (typeof visibility !== 'undefined') { - payload['visibility'] = visibility - } - return promisedRequest({ - url, - credentials, - method: 'PUT', - payload, - }) -} - -const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => { - const payload = { content } - - if (typeof startsAt !== 'undefined') { - payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null - } - - if (typeof endsAt !== 'undefined') { - payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null - } - - if (typeof allDay !== 'undefined') { - payload.all_day = allDay - } - - return payload -} - -const postAnnouncement = ({ - credentials, - content, - startsAt, - endsAt, - allDay, -}) => { - return promisedRequest({ - url: PLEROMA_POST_ANNOUNCEMENT_URL, - credentials, - method: 'POST', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) -} - -const editAnnouncement = ({ - id, - credentials, - content, - startsAt, - endsAt, - allDay, -}) => { - return promisedRequest({ - url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id), - credentials, - method: 'PATCH', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) -} - -const deleteAnnouncement = ({ id, credentials }) => { - return promisedRequest({ - url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id), - credentials, - method: 'DELETE', - }) -} - -export const getMastodonSocketURI = ( - { credentials, stream, args = {} }, - base, -) => { - const url = new URL(MASTODON_STREAMING, base) - if (credentials) { - url.searchParams.append('access_token', credentials) - } - if (stream) { - url.searchParams.append('stream', stream) - } - Object.entries(args).forEach(([key, val]) => { - url.searchParams.append(key, val) - }) - return url -} - -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, -}) - -const chats = ({ credentials }) => { - return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => { - return { chats: data.map(parseChat).filter((c) => c) } - }) -} - -const getOrCreateChat = ({ accountId, credentials }) => { - return promisedRequest({ - url: PLEROMA_CHAT_URL(accountId), - method: 'POST', - credentials, - }) -} - -const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => { - let url = PLEROMA_CHAT_MESSAGES_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - ] - .filter((_) => _) - .join('&') - - url = url + (args ? '?' + args : '') - - return promisedRequest({ - url, - method: 'GET', - credentials, - }) -} - -const sendChatMessage = ({ - id, - content, - mediaId = null, - idempotencyKey, - credentials, -}) => { - const payload = { - content, - } - - if (mediaId) { - payload.media_id = mediaId - } - - const headers = {} - - if (idempotencyKey) { - headers['idempotency-key'] = idempotencyKey - } - - return promisedRequest({ - url: PLEROMA_CHAT_MESSAGES_URL(id), - method: 'POST', - payload, - credentials, - headers, - }) -} - -const readChat = ({ id, lastReadId, credentials }) => { - return promisedRequest({ - url: PLEROMA_CHAT_READ_URL(id), - method: 'POST', - payload: { - last_read_id: lastReadId, - }, - credentials, - }) -} - -const deleteChatMessage = ({ chatId, messageId, credentials }) => { - return promisedRequest({ - url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), - method: 'DELETE', - credentials, - }) -} - -const setReportState = ({ id, state, credentials }) => { - // TODO: Can't use promisedRequest because on OK this does not return json - // See https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1322 - return fetch(PLEROMA_ADMIN_REPORTS, { - headers: { - ...authHeaders(credentials), - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'PATCH', - body: JSON.stringify({ - reports: [ - { - id, - state, - }, - ], - }), - }) - .then((data) => { - if (data.status >= 500) { - throw Error(data.statusText) - } else if (data.status >= 400) { - return data.json() - } - return data - }) - .then((data) => { - if (data.errors) { - throw Error(data.errors[0].message) - } - }) -} - -// ADMIN STUFF // EXPERIMENTAL -const fetchInstanceDBConfig = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_CONFIG_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchInstanceConfigDescriptions = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchAvailableFrontends = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_FRONTENDS_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const pushInstanceDBConfig = ({ credentials, payload }) => { - return fetch(PLEROMA_ADMIN_CONFIG_URL, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...authHeaders(credentials), - }, - method: 'POST', - body: JSON.stringify(payload), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const installFrontend = ({ credentials, payload }) => { - return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...authHeaders(credentials), - }, - method: 'POST', - body: JSON.stringify(payload), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchScrobbles = ({ accountId, limit = 1 }) => { - let url = PLEROMA_SCROBBLES_URL(accountId) - const params = [['limit', limit]] - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - return fetch(url, {}).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const deleteEmojiPack = ({ name }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' }) -} - -const reloadEmoji = () => { - return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' }) -} - -const importEmojiFromFS = () => { - return fetch(PLEROMA_EMOJI_IMPORT_FS_URL) -} - -const createEmojiPack = ({ name }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' }) -} - -const listEmojiPacks = ({ page, pageSize }) => { - return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize)) -} - -const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { - if (!instance.startsWith('http')) { - instance = 'https://' + instance - } - - return fetch(PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), { - headers: { 'Content-Type': 'application/json' }, - }) -} - -const downloadRemoteEmojiPack = ({ instance, packName, as }) => { - return fetch(PLEROMA_EMOJI_PACKS_DL_REMOTE_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - url: instance, - name: packName, - as, - }), - }) -} - -const downloadRemoteEmojiPackZIP = ({ url, packName, file }) => { - const data = new FormData() - if (file) data.set('file', file) - if (url) data.set('url', url) - data.set('name', packName) - - return fetch(PLEROMA_EMOJI_PACKS_DL_REMOTE_ZIP_URL, { - method: 'POST', - body: data, - }) -} - -const saveEmojiPackMetadata = ({ name, newData }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ metadata: newData }), - }) -} - -const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { - const data = new FormData() - if (filename.trim() !== '') { - data.set('filename', filename) - } - if (shortcode.trim() !== '') { - data.set('shortcode', shortcode) - } - data.set('file', file) - - return fetch(PLEROMA_EMOJI_UPDATE_FILE_URL(packName), { - method: 'POST', - body: data, - }) -} - -const updateEmojiFile = ({ - packName, - shortcode, - newShortcode, - newFilename, - force, -}) => { - return fetch(PLEROMA_EMOJI_UPDATE_FILE_URL(packName), { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - shortcode, - new_shortcode: newShortcode, - new_filename: newFilename, - force, - }), - }) -} - -const deleteEmojiFile = ({ packName, shortcode }) => { - return fetch( - `${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, - { method: 'DELETE' }, - ) -} - -const fetchBookmarkFolders = ({ credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDERS_URL - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const createBookmarkFolder = ({ name, emoji, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDERS_URL - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ name, emoji }), - }).then((data) => data.json()) -} - -const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'PATCH', - body: JSON.stringify({ name, emoji }), - }).then((data) => data.json()) -} - -const deleteBookmarkFolder = ({ folderId, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) - return fetch(url, { - method: 'DELETE', - headers: authHeaders(credentials), - }) -} - -const apiService = { - verifyCredentials, - fetchTimeline, - fetchPinnedStatuses, - fetchConversation, - fetchStatus, - fetchStatusSource, - fetchStatusHistory, - fetchFriends, - exportFriends, - fetchFollowers, - followUser, - unfollowUser, - pinOwnStatus, - unpinOwnStatus, - muteConversation, - unmuteConversation, - blockUser, - unblockUser, - removeUserFromFollowers, - editUserNote, - fetchUser, - fetchUserByName, - fetchUserRelationship, - favorite, - unfavorite, - retweet, - unretweet, - bookmarkStatus, - unbookmarkStatus, - postStatus, - editStatus, - deleteStatus, - uploadMedia, - setMediaDescription, - fetchMutes, - muteUser, - unmuteUser, - fetchBlocks, - fetchOAuthTokens, - revokeOAuthToken, - register, - getCaptcha, - updateProfileImages, - updateProfile, - updateProfileJSON, - importMutes, - importBlocks, - importFollows, - deleteAccount, - changeEmail, - moveAccount, - addAlias, - deleteAlias, - listAliases, - changePassword, - settingsMFA, - mfaDisableOTP, - generateMfaBackupCodes, - mfaSetupOTP, - mfaConfirmOTP, - addBackup, - listBackups, - fetchFollowRequests, - fetchLists, - createList, - getList, - updateList, - getListAccounts, - addAccountsToList, - removeAccountsFromList, - deleteList, - approveUser, - denyUser, - suggestions, - markNotificationsAsSeen, - dismissNotification, - vote, - fetchPoll, - fetchFavoritedByUsers, - fetchRebloggedByUsers, - fetchEmojiReactions, - reactWithEmoji, - unreactWithEmoji, - reportUser, - updateNotificationSettings, - search2, - searchUsers, - fetchKnownDomains, - fetchDomainMutes, - muteDomain, - unmuteDomain, - chats, - getOrCreateChat, - chatMessages, - sendChatMessage, - readChat, - deleteChatMessage, - setReportState, - fetchUserInLists, - fetchAnnouncements, - dismissAnnouncement, - postAnnouncement, - editAnnouncement, - deleteAnnouncement, - fetchScrobbles, - adminFetchAnnouncements, - fetchInstanceDBConfig, - fetchInstanceConfigDescriptions, - fetchAvailableFrontends, - pushInstanceDBConfig, - installFrontend, - importEmojiFromFS, - reloadEmoji, - listEmojiPacks, - createEmojiPack, - deleteEmojiPack, - saveEmojiPackMetadata, - addNewEmojiFile, - updateEmojiFile, - deleteEmojiFile, - listRemoteEmojiPacks, - downloadRemoteEmojiPack, - downloadRemoteEmojiPackZIP, - fetchBookmarkFolders, - createBookmarkFolder, - updateBookmarkFolder, - deleteBookmarkFolder, - adminListUsers, - adminGetUserData, - adminResendConfirmationEmail, - adminDeleteAccounts, - adminSetUsersRight, - adminSetUsersTags, - adminSetUsersApprovalStatus, - adminSetUsersConfirmationStatus, - adminSetUsersActivationStatus, - adminSetUsersSuggestionStatus, - adminListStatuses, - adminChangeStatusScope, - adminRequirePasswordChange, - adminDisableMFA, -} - -export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js deleted file mode 100644 index adc18ef7f..000000000 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ /dev/null @@ -1,75 +0,0 @@ -import bookmarkFoldersFetcher from '../../services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js' -import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' -import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' -import apiService, { - getMastodonSocketURI, - ProcessedWS, -} from '../api/api.service.js' -import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' -import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' - -import { useInstanceStore } from 'src/stores/instance.js' - -const backendInteractorService = (credentials) => ({ - startFetchingTimeline({ - timeline, - store, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag, - }) { - return timelineFetcher.startFetching({ - timeline, - store, - credentials, - userId, - listId, - statusId, - bookmarkFolderId, - tag, - }) - }, - - fetchTimeline(args) { - return timelineFetcher.fetchAndUpdate({ ...args, credentials }) - }, - - startFetchingNotifications({ store }) { - return notificationsFetcher.startFetching({ store, credentials }) - }, - - fetchNotifications(args) { - return notificationsFetcher.fetchAndUpdate({ ...args, credentials }) - }, - - startFetchingFollowRequests({ store }) { - return followRequestFetcher.startFetching({ store, credentials }) - }, - - startFetchingLists({ store }) { - return listsFetcher.startFetching({ store, credentials }) - }, - - startFetchingBookmarkFolders({ store }) { - return bookmarkFoldersFetcher.startFetching({ store, credentials }) - }, - - startUserSocket({ store }) { - const serv = useInstanceStore().server.replace('http', 'ws') - const url = getMastodonSocketURI({}, serv) - return ProcessedWS({ url, id: 'Unified', credentials }) - }, - - ...Object.entries(apiService).reduce((acc, [key, func]) => { - return { - ...acc, - [key]: (args) => func({ credentials, ...args }), - } - }, {}), - - verifyCredentials: apiService.verifyCredentials, -}) - -export default backendInteractorService diff --git a/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js b/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js deleted file mode 100644 index 7b81c19dc..000000000 --- a/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js +++ /dev/null @@ -1,32 +0,0 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' - -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' - -const fetchAndUpdate = ({ credentials }) => { - return apiService - .fetchBookmarkFolders({ credentials }) - .then( - (bookmarkFolders) => { - useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders) - }, - (rej) => { - console.error(rej) - }, - ) - .catch((e) => { - console.error(e) - }) -} - -const startFetching = ({ credentials, store }) => { - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - boundFetchAndUpdate() - return promiseInterval(boundFetchAndUpdate, 240000) -} - -const bookmarkFoldersFetcher = { - startFetching, -} - -export default bookmarkFoldersFetcher diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 41829eb19..ccbae9b3e 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -13,8 +13,10 @@ function humanizeErrors(errors) { export function StatusCodeError(statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode - this.message = - statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body) + this.statusText = body.error || body.errors || body + this.details = JSON && JSON.stringify ? JSON.stringify(body) : body + this.errorData = body.error || body.errors + this.message = this.statusCode + ' - ' + this.statusText this.error = body // legacy attribute this.options = options this.response = response diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index 209a5f0c0..759ba67d9 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -1,9 +1,19 @@ +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + fetchUserRelationship, + followUser, + unfollowUser, +} from 'src/api/user.js' + const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => { setTimeout(() => { - store.state.api.backendInteractor - .fetchUserRelationship({ id: userId }) - .then((relationship) => { + fetchUserRelationship({ + id: userId, + credentials: useOAuthStore().token, + }) + .then(({ data: relationship }) => { store.commit('updateUserRelationship', [relationship]) return relationship }) @@ -25,40 +35,33 @@ const fetchRelationship = (attempt, userId, store) => } }) -export const requestFollow = (userId, store) => - new Promise((resolve) => { - store.state.api.backendInteractor - .followUser({ id: userId }) - .then((updated) => { - store.commit('updateUserRelationship', [updated]) - - if (updated.following || (updated.locked && updated.requested)) { - // If we get result immediately or the account is locked, just stop. - resolve() - return - } - - // But usually we don't get result immediately, so we ask server - // for updated user profile to confirm if we are following them - // Sometimes it takes several tries. Sometimes we end up not following - // user anyway, probably because they locked themselves and we - // don't know that yet. - // Recursive Promise, it will call itself up to 3 times. - - return fetchRelationship(1, updated, store).then(() => { - resolve() - }) - }) +export const requestFollow = async (userId, store) => { + const { data: updated } = await followUser({ + id: userId, + credentials: useOAuthStore().token, }) -export const requestUnfollow = (userId, store) => - new Promise((resolve) => { - store.state.api.backendInteractor - .unfollowUser({ id: userId }) - .then((updated) => { - store.commit('updateUserRelationship', [updated]) - resolve({ - updated, - }) - }) + store.commit('updateUserRelationship', [updated]) + + if (updated.following || (updated.locked && updated.requested)) { + // If we get result immediately or the account is locked, just stop. + return + } + + // But usually we don't get result immediately, so we ask server + // for updated user profile to confirm if we are following them + // Sometimes it takes several tries. Sometimes we end up not following + // user anyway, probably because they locked themselves and we + // don't know that yet. + // Recursive Promise, it will call itself up to 3 times. + return await fetchRelationship(1, updated, store) +} + +export const requestUnfollow = async (userId, store) => { + const { data: updated } = await unfollowUser({ + id: userId, + credentials: useOAuthStore().token, }) + + return await store.commit('updateUserRelationship', [updated]) +} diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js index 530c98aa7..e49206fcd 100644 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -1,11 +1,10 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' +import { fetchFollowRequests } from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' const fetchAndUpdate = ({ store, credentials }) => { - return apiService - .fetchFollowRequests({ credentials }) + return fetchFollowRequests({ credentials }) .then( - (requests) => { + ({ data: requests }) => { store.commit('setFollowRequests', requests) store.commit('addNewUsers', requests) }, diff --git a/src/services/lists_fetcher/lists_fetcher.service.js b/src/services/lists_fetcher/lists_fetcher.service.js deleted file mode 100644 index c395ef93b..000000000 --- a/src/services/lists_fetcher/lists_fetcher.service.js +++ /dev/null @@ -1,32 +0,0 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' - -import { useListsStore } from 'src/stores/lists.js' - -const fetchAndUpdate = ({ credentials }) => { - return apiService - .fetchLists({ credentials }) - .then( - (lists) => { - useListsStore().setLists(lists) - }, - (rej) => { - console.error(rej) - }, - ) - .catch((e) => { - console.error(e) - }) -} - -const startFetching = ({ credentials, store }) => { - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - boundFetchAndUpdate() - return promiseInterval(boundFetchAndUpdate, 240000) -} - -const listsFetcher = { - startFetching, -} - -export default listsFetcher diff --git a/src/services/new_api/mfa.js b/src/services/new_api/mfa.js deleted file mode 100644 index 1d35b65a8..000000000 --- a/src/services/new_api/mfa.js +++ /dev/null @@ -1,54 +0,0 @@ -const verifyOTPCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'totp') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const verifyRecoveryCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'recovery') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const mfa = { - verifyOTPCode, - verifyRecoveryCode, -} - -export default mfa diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js deleted file mode 100644 index b803e2146..000000000 --- a/src/services/new_api/oauth.js +++ /dev/null @@ -1,198 +0,0 @@ -import { reduce } from 'lodash' - -import { StatusCodeError } from 'src/services/errors/errors.js' - -const REDIRECT_URI = `${window.location.origin}/oauth-callback` - -export const getJsonOrError = async (response) => { - if (response.ok) { - return response.json().catch((error) => { - throw new StatusCodeError(response.status, error, {}, response) - }) - } else { - throw new StatusCodeError( - response.status, - await response.text(), - {}, - response, - ) - } -} - -export const createApp = (instance) => { - const url = `${instance}/api/v1/apps` - const form = new window.FormData() - - form.append('client_name', 'PleromaFE') - form.append('website', 'https://pleroma.social') - form.append('redirect_uris', REDIRECT_URI) - form.append('scopes', 'read write follow push admin') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then(getJsonOrError) - .then((app) => ({ - clientId: app.client_id, - clientSecret: app.client_secret, - })) -} - -export const verifyAppToken = ({ instance, appToken }) => { - return window - .fetch(`${instance}/api/v1/apps/verify_credentials`, { - method: 'GET', - headers: { Authorization: `Bearer ${appToken}` }, - }) - .then(getJsonOrError) -} - -const login = ({ instance, clientId }) => { - const data = { - response_type: 'code', - client_id: clientId, - redirect_uri: REDIRECT_URI, - scope: 'read write follow push admin', - } - - const dataString = reduce( - data, - (acc, v, k) => { - const encoded = `${k}=${encodeURIComponent(v)}` - if (!acc) { - return encoded - } else { - return `${acc}&${encoded}` - } - }, - false, - ) - - // Do the redirect... - const url = `${instance}/oauth/authorize?${dataString}` - - window.location.href = url -} - -const getTokenWithCredentials = ({ - clientId, - clientSecret, - instance, - username, - password, -}) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'password') - form.append('username', username) - form.append('password', password) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const getToken = ({ clientId, clientSecret, instance, code }) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'authorization_code') - form.append('code', code) - form.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -export const getClientToken = ({ clientId, clientSecret, instance }) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'client_credentials') - form.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then(getJsonOrError) -} -const verifyOTPCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'totp') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'recovery') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const revokeToken = ({ app, instance, token }) => { - const url = `${instance}/oauth/revoke` - const form = new window.FormData() - - form.append('client_id', app.clientId) - form.append('client_secret', app.clientSecret) - form.append('token', token) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const oauth = { - login, - getToken, - getTokenWithCredentials, - verifyOTPCode, - verifyRecoveryCode, - revokeToken, -} - -export default oauth diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js deleted file mode 100644 index 65342c04b..000000000 --- a/src/services/new_api/password_reset.js +++ /dev/null @@ -1,22 +0,0 @@ -import { reduce } from 'lodash' - -const MASTODON_PASSWORD_RESET_URL = '/auth/password' - -const resetPassword = ({ instance, email }) => { - const params = { email } - const query = reduce( - params, - (acc, v, k) => { - const encoded = `${k}=${encodeURIComponent(v)}` - return `${acc}&${encoded}` - }, - '', - ) - const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}` - - return window.fetch(url, { - method: 'POST', - }) -} - -export default resetPassword diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index e7987146a..37e5e95ad 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -1,8 +1,6 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' import { muteFilterHits } from '../status_parser/status_parser.js' -import { useAnnouncementsStore } from 'src/stores/announcements.js' - import FaviconService from 'src/services/favicon_service/favicon_service.js' export const ACTIONABLE_NOTIFICATION_TYPES = new Set([ diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index c1a9e1a2f..8530c468c 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,11 +1,11 @@ -import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { fetchTimeline } from 'src/api/timelines.js' + const update = ({ store, notifications, older }) => { store.dispatch('addNewNotifications', { notifications, older }) } @@ -25,7 +25,7 @@ const mastoApiNotificationTypes = new Set([ 'pleroma:report', ]) -const fetchAndUpdate = ({ store, credentials, older = false, since }) => { +const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { const args = { credentials } const rootState = store.rootState || store.state const timelineData = rootState.notifications @@ -35,24 +35,24 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { mastoApiNotificationTypes.add('pleroma:chat_mention') } - args.includeTypes = mastoApiNotificationTypes + args.includeTypes = [...mastoApiNotificationTypes] args.withMuted = !hideMutedPosts args.timeline = 'notifications' if (older) { if (timelineData.minId !== Number.POSITIVE_INFINITY) { - args.until = timelineData.minId + args.maxId = timelineData.minId } return fetchNotifications({ store, args, older }) } else { // fetch new notifications if ( - since === undefined && + sinceId === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY ) { - args.since = timelineData.maxId - } else if (since !== null) { - args.since = since + args.sinceId = timelineData.maxId + } else if (sinceId !== null) { + args.sinceId = sinceId } const result = fetchNotifications({ store, args, older }) @@ -69,7 +69,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { if (readNotifsIds.length > 0 && readNotifsIds.length > 0) { const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification if (minId !== Infinity) { - args.since = false // Don't use since_id since it sorta conflicts with min_id + args.sinceId = null // Don't use since_id since it sorta conflicts with min_id args.minId = minId - 1 // go beyond fetchNotifications({ store, args, older }) } @@ -80,29 +80,25 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { } const fetchNotifications = ({ store, args, older }) => { - return apiService - .fetchTimeline(args) + return fetchTimeline(args) .then((response) => { - if (response.errors) { - if ( - response.status === 400 && - response.statusText.includes('Invalid value for enum') - ) { - response.statusText - .matchAll(/(\w+) - Invalid value for enum./g) - .toArray() - .map((x) => x[1]) - .forEach((x) => mastoApiNotificationTypes.delete(x)) - return fetchNotifications({ store, args, older }) - } else { - throw new Error(`${response.status} ${response.statusText}`) - } - } const notifications = response.data update({ store, notifications, older }) return notifications }) .catch((error) => { + if ( + error.statusCode === 400 && + error.statusText.includes('Invalid value for enum') + ) { + error.statusText + .matchAll(/(\w+) - Invalid value for enum./g) + .toArray() + .map((x) => x[1]) + .forEach((x) => mastoApiNotificationTypes.delete(x)) + return fetchNotifications({ store, args, older }) + } + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'notifications.error', diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js index 46ac68996..d9396c643 100644 --- a/src/services/promise_interval/promise_interval.js +++ b/src/services/promise_interval/promise_interval.js @@ -4,32 +4,34 @@ // time after the first interval. // - interval is the interval delay in ms. +const wait = (timeout) => { + let timeoutId + const promise = () => + new Promise((resolve) => { + timeoutId = window.setTimeout(() => resolve(), timeout) + }) + return { timeoutId, promise } +} + export const promiseInterval = (promiseCall, interval) => { let stopped = false let timeout = null - const func = () => { - const promise = promiseCall() - // something unexpected happened and promiseCall did not - // return a promise, abort the loop. - if (!(promise && promise.finally)) { - console.warn( - 'promiseInterval: promise call did not return a promise, stopping interval.', - ) - return - } - promise.finally(() => { - if (stopped) return - timeout = window.setTimeout(func, interval) - }) - } - const stopFetcher = () => { stopped = true window.clearTimeout(timeout) } - timeout = window.setTimeout(func, interval) + const loop = async () => { + while (!stopped) { + await promiseCall() + const { timeoutId, promise } = wait(interval) + timeout = timeoutId + await promise() + } + } + + loop().then() return { stop: stopFetcher } } diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index 021c31ef8..9a26bd12f 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -1,6 +1,11 @@ import { map } from 'lodash' -import apiService from '../api/api.service.js' +import { + editStatus as apiEditStatus, + postStatus as apiPostStatus, + setMediaDescription as apiSetMediaDescription, + uploadMedia as apiUploadMedia, +} from 'src/api/user.js' const postStatus = ({ store, @@ -18,37 +23,30 @@ const postStatus = ({ }) => { const mediaIds = map(media, 'id') - return apiService - .postStatus({ - credentials: store.state.users.currentUser.credentials, - status, - spoilerText, - visibility, - sensitive, - mediaIds, - inReplyToStatusId, - quoteId, - contentType, - poll, - preview, - idempotencyKey, - }) - .then((data) => { - if (!data.error && !preview) { - store.dispatch('addNewStatuses', { - statuses: [data], - timeline: 'friends', - showImmediately: true, - noIdUpdate: true, // To prevent missing notices on next pull. - }) - } - return data - }) - .catch((err) => { - return { - error: err.message, - } - }) + return apiPostStatus({ + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + visibility, + sensitive, + mediaIds, + inReplyToStatusId, + quoteId, + contentType, + poll, + preview, + idempotencyKey, + }).then(({ data }) => { + if (!preview) + store.dispatch('addNewStatuses', { + statuses: [data], + timeline: 'friends', + showImmediately: true, + noIdUpdate: true, // To prevent missing notices on next pull. + }) + + return data + }) } const editStatus = ({ @@ -63,26 +61,24 @@ const editStatus = ({ }) => { const mediaIds = map(media, 'id') - return apiService - .editStatus({ - id: statusId, - credentials: store.state.users.currentUser.credentials, - status, - spoilerText, - sensitive, - poll, - mediaIds, - contentType, - }) - .then((data) => { - if (!data.error) { - store.dispatch('addNewStatuses', { - statuses: [data], - timeline: 'friends', - showImmediately: true, - noIdUpdate: true, // To prevent missing notices on next pull. - }) - } + return apiEditStatus({ + id: statusId, + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds, + contentType, + }) + .then(({ data }) => { + store.dispatch('addNewStatuses', { + statuses: [data], + timeline: 'friends', + showImmediately: true, + noIdUpdate: true, // To prevent missing notices on next pull. + }) + return data }) .catch((err) => { @@ -95,12 +91,14 @@ const editStatus = ({ const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials - return apiService.uploadMedia({ credentials, formData }) + return apiUploadMedia({ credentials, formData }).then(({ data }) => data) } const setMediaDescription = ({ store, id, description }) => { const credentials = store.state.users.currentUser.credentials - return apiService.setMediaDescription({ credentials, id, description }) + return apiSetMediaDescription({ credentials, id, description }).then( + ({ data }) => data, + ) } const statusPosterService = { diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 68991addf..80dbc75d0 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -1,13 +1,13 @@ import { camelCase } from 'lodash' -import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { fetchTimeline } from 'src/api/timelines.js' + const update = ({ store, statuses, @@ -35,13 +35,13 @@ const fetchAndUpdate = ({ timeline = 'friends', older = false, showImmediately = false, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag = false, - until, - since, + userId, + listId, + statusId, + bookmarkFolderId, + tag, + maxId, + sinceId, }) => { const args = { timeline, credentials } const rootState = store.rootState || store.state @@ -51,12 +51,13 @@ const fetchAndUpdate = ({ const loggedIn = !!rootState.users.currentUser if (older) { - args.until = until || timelineData.minId + // When minId = 0 we need to fetch without maxId param + args.maxId = maxId || timelineData.minId || null } else { - if (since === undefined) { - args.since = timelineData.maxId - } else if (since !== null) { - args.since = since + if (sinceId === undefined) { + args.sinceId = timelineData.maxId + } else if (sinceId !== null) { + args.sinceId = sinceId } } @@ -75,17 +76,8 @@ const fetchAndUpdate = ({ const numStatusesBeforeFetch = timelineData.statuses.length - return apiService - .fetchTimeline(args) + return fetchTimeline(args) .then((response) => { - if (response.errors) { - if (timeline === 'favorites') { - useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false - return - } - throw new Error(`${response.status} ${response.statusText}`) - } - const { data: statuses, pagination } = response if ( !older && @@ -107,6 +99,10 @@ const fetchAndUpdate = ({ return { statuses, pagination } }) .catch((error) => { + if (error.statusCode === 403 && timeline === 'favorites') { + useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false + return + } useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.error', @@ -120,11 +116,11 @@ const startFetching = ({ timeline = 'friends', credentials, store, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag = false, + userId, + listId, + statusId, + bookmarkFolderId, + tag, }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index b07315034..5aa91c819 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -1,6 +1,38 @@ import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + addNewEmojiFile, + changeStatusScope, + createEmojiPack, + deleteAccounts, + deleteEmojiPack, + disableMFA, + downloadRemoteEmojiPack, + downloadRemoteEmojiPackZIP, + getAvailableFrontends, + getInstanceConfigDescriptions, + getInstanceDBConfig, + getUserData, + importEmojiFromFS, + installFrontend, + listRemoteEmojiPacks, + listStatuses, + listUsers, + pushInstanceDBConfig, + reloadEmoji, + requirePasswordChange, + resendConfirmationEmail, + setUsersActivationStatus, + setUsersApprovalStatus, + setUsersConfirmationStatus, + setUsersRight, + setUsersSuggestionStatus, + setUsersTags, +} from 'src/api/admin.js' +import { listEmojiPacks } from 'src/api/public.js' import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js' export const defaultState = { @@ -21,7 +53,6 @@ export const newUserFlags = { export const useAdminSettingsStore = defineStore('adminSettings', { state: () => ({ ...cloneDeep(defaultState), - backendInteractor: window.vuex.state.api.backendInteractor, }), actions: { // Configuration Stuff @@ -54,25 +85,31 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }, loadAdminStuff() { - this.backendInteractor.fetchInstanceDBConfig().then((backendDbConfig) => { - if (backendDbConfig.error) { - if (backendDbConfig.error.status === 400) { - backendDbConfig.error.json().then((errorJson) => { - if (/configurable_from_database/.test(errorJson.error)) { - this.setInstanceAdminNoDbConfig() - } - }) - } - } else { - this.setInstanceAdminSettings({ backendDbConfig }) - } + getInstanceDBConfig({ + credentials: useOAuthStore().token, }) + .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) { - this.backendInteractor - .fetchInstanceConfigDescriptions() - .then((backendDescriptions) => - this.setInstanceAdminDescriptions({ backendDescriptions }), - ) + getInstanceConfigDescriptions({ + credentials: useOAuthStore().token, + }).then(({ data: backendDescriptions }) => + this.setInstanceAdminDescriptions({ + credentials: useOAuthStore().token, + backendDescriptions, + }), + ) } }, setInstanceAdminSettings({ backendDbConfig }) { @@ -203,17 +240,23 @@ export const useAdminSettingsStore = defineStore('adminSettings', { } }) - window.vuex.state.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: changed, - }, - }) + pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: changed, + }, + }) .then(() => - window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(), + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), ) .then((backendDbConfig) => - this.setInstanceAdminSettings({ backendDbConfig }), + this.setInstanceAdminSettings({ + credentials: useOAuthStore().token, + + backendDbConfig, + }), ) }, pushAdminSetting({ path, value }) { @@ -234,23 +277,28 @@ export const useAdminSettingsStore = defineStore('adminSettings', { } } - window.vuex.state.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: [ - { - group, - key, - value: convert(clone), - }, - ], - }, - }) + pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: [ + { + group, + key, + value: convert(clone), + }, + ], + }, + }) .then(() => - window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(), + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), ) .then((backendDbConfig) => - this.setInstanceAdminSettings({ backendDbConfig }), + this.setInstanceAdminSettings({ + credentials: useOAuthStore().token, + backendDbConfig, + }), ) }, resetAdminSetting({ path }) { @@ -260,21 +308,23 @@ export const useAdminSettingsStore = defineStore('adminSettings', { this.modifiedPaths.delete(path) - return window.vuex.state.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: [ - { - group, - key, - delete: true, - subkeys: [subkey], - }, - ], - }, - }) + return pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: [ + { + group, + key, + delete: true, + subkeys: [subkey], + }, + ], + }, + }) .then(() => - window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(), + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), ) .then((backendDbConfig) => this.setInstanceAdminSettings({ backendDbConfig }), @@ -283,9 +333,11 @@ export const useAdminSettingsStore = defineStore('adminSettings', { // Frontends Stuff loadFrontendsStuff() { - this.backendInteractor - .fetchAvailableFrontends() - .then((frontends) => this.setAvailableFrontends({ frontends })) + getAvailableFrontends({ + credentials: useOAuthStore().token, + }).then(({ data: frontends }) => + this.setAvailableFrontends({ frontends }), + ) }, setAvailableFrontends({ frontends }) { @@ -300,12 +352,20 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }) }, + installFrontend() { + return installFrontend({ + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + // Statuses stuff async fetchStatuses(opts) { - const { total, activities } = - await this.backendInteractor.adminListStatuses({ - opts, - }) + const { + data: { total, activities }, + } = await listStatuses({ + credentials: useOAuthStore().token, + opts, + }) const statuses = activities.map(parseStatus) @@ -317,17 +377,21 @@ export const useAdminSettingsStore = defineStore('adminSettings', { } }, async changeStatusScope(opts) { - const raw = await this.backendInteractor.adminChangeStatusScope({ + const { data } = await changeStatusScope({ + credentials: useOAuthStore().token, opts, }) - const status = parseStatus(raw) + const status = parseStatus(data) await window.vuex.dispatch('addNewStatuses', { statuses: [status] }) }, // Users stuff async fetchUsers(opts) { - const { users, count } = await this.backendInteractor.adminListUsers({ + const { + data: { users, count }, + } = await listUsers({ + credentials: useOAuthStore().token, opts, }) @@ -344,19 +408,26 @@ export const useAdminSettingsStore = defineStore('adminSettings', { } }, async getUserData({ user }) { - const api = this.backendInteractor.adminGetUserData + const api = getUserData const { screen_name } = user - const result = await api({ screen_name }) - window.vuex.commit('updateUserAdminData', { user: result }) + const result = await api({ + credentials: useOAuthStore().token, + screen_name, + }) + + window.vuex.commit('updateUserAdminData', { user: result.data }) }, async deleteUsers({ users }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminDeleteAccounts + const api = deleteAccounts - const resultUserIds = await api({ screen_names }) + const resultUserIds = await api({ + credentials: useOAuthStore().token, + screen_names, + }) - resultUserIds.forEach((userId) => { + resultUserIds.data.forEach((userId) => { window.vuex.dispatch( 'markStatusesAsDeleted', (status) => userId === status.user.id, @@ -369,28 +440,34 @@ export const useAdminSettingsStore = defineStore('adminSettings', { resendConfirmationEmail({ users }) { const screen_names = users.map((u) => u.screen_name) - return this.backendInteractor.adminResendConfirmationEmail({ + return resendConfirmationEmail({ + credentials: useOAuthStore().token, screen_names, - }) + }).then(({ data }) => data) }, requirePasswordChange({ users }) { const screen_names = users.map((u) => u.screen_name) - return this.backendInteractor.adminRequirePasswordChange({ + return requirePasswordChange({ + credentials: useOAuthStore().token, screen_names, - }) + }).then(({ data }) => data) }, // Singular only! disableMFA({ user }) { const { screen_name } = user - return this.backendInteractor.adminDisableMFA({ screen_name }) + return disableMFA({ + credentials: useOAuthStore().token, + screen_name, + }).then(({ data }) => data) }, async setUsersTags({ users, tags, value }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersTags + const api = setUsersTags await api({ + credentials: useOAuthStore().token, screen_names, tags, value, @@ -402,9 +479,10 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }, async setUsersRight({ users, right, value }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersRight + const api = setUsersRight await api({ + credentials: useOAuthStore().token, screen_names, right, value, @@ -416,35 +494,40 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }, async setUsersActivationStatus({ users, value }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersActivationStatus + const api = setUsersActivationStatus const resultUsers = await api({ + credentials: useOAuthStore().token, screen_names, value, }) - resultUsers.forEach((user) => { + resultUsers.data.forEach((user) => { window.vuex.commit('updateUserAdminData', { user }) }) }, async setUsersSuggestionStatus({ users, value }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersSuggestionStatus + const api = setUsersSuggestionStatus const resultUsers = await api({ + credentials: useOAuthStore().token, screen_names, value, }) - resultUsers.forEach((user) => { + resultUsers.data.forEach((user) => { window.vuex.commit('updateUserAdminData', { user }) }) }, async setUsersConfirmationStatus({ users }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersConfirmationStatus + const api = setUsersConfirmationStatus - await api({ screen_names }) + await api({ + credentials: useOAuthStore().token, + screen_names, + }) users.forEach((user) => { this.getUserData({ user }) @@ -452,15 +535,81 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }, async setUsersApprovalStatus({ users }) { const screen_names = users.map((u) => u.screen_name) - const api = this.backendInteractor.adminSetUsersApprovalStatus + const api = setUsersApprovalStatus const resultUsers = await api({ + credentials: useOAuthStore().token, screen_names, }) - resultUsers.forEach((user) => { + resultUsers.data.forEach((user) => { window.vuex.commit('updateUserAdminData', { user }) }) }, + reloadEmoji() { + return reloadEmoji({ credentials: useOAuthStore().token }).then( + ({ data }) => data, + ) + }, + importEmojiFromFS() { + return importEmojiFromFS({ credentials: useOAuthStore().token }).then( + ({ data }) => data, + ) + }, + listEmojiPacks(params) { + return listEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + listRemoteEmojiPacks(params) { + return listRemoteEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + addNewEmojiFile({ packName, file, shortcode, filename }) { + return addNewEmojiFile({ + packName, + file, + shortcode, + filename, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + downloadRemoteEmojiPack({ instance, packName, as }) { + return downloadRemoteEmojiPack({ + instance, + packName, + as, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + downloadRemoteEmojiPackZIP({ url, packName }) { + return downloadRemoteEmojiPackZIP({ + url, + packName, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + createEmojiPack({ name }) { + return createEmojiPack({ + name, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + deleteEmojiPack({ name }) { + return deleteEmojiPack({ + name, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + saveEmojiPackMetadata({ name, newData }) { + return createEmojiPack({ + name, + newData, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, }, }) diff --git a/src/stores/announcements.js b/src/stores/announcements.js index cb325dadd..a5f3e4d8e 100644 --- a/src/stores/announcements.js +++ b/src/stores/announcements.js @@ -1,5 +1,9 @@ import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { dismissAnnouncement, getAnnouncements } from 'src/api/user.js' + const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5 export const useAnnouncementsStore = defineStore('announcements', { @@ -7,6 +11,8 @@ export const useAnnouncementsStore = defineStore('announcements', { announcements: [], supportsAnnouncements: true, fetchAnnouncementsTimer: undefined, + adminActions: {}, + userActions: {}, }), getters: { unreadAnnouncementCount() { @@ -21,29 +27,37 @@ export const useAnnouncementsStore = defineStore('announcements', { }, }, actions: { - fetchAnnouncements() { - if (!this.supportsAnnouncements) { - return Promise.resolve() - } + async fetchAnnouncements() { + if (!this.supportsAnnouncements) return const currentUser = window.vuex.state.users.currentUser const isAdmin = currentUser && currentUser.privileges.has('announcements_manage_announcements') - const getAnnouncements = async () => { - if (!isAdmin) { - return window.vuex.state.api.backendInteractor.fetchAnnouncements() + try { + if (isAdmin) { + this.adminActions = await import('src/api/admin.js') + } else { + const all = await getAnnouncements({ + credentials: useOAuthStore().token, + }) + return all.data } - const all = - await window.vuex.state.api.backendInteractor.adminFetchAnnouncements() - const visible = - await window.vuex.state.api.backendInteractor.fetchAnnouncements() + const { data: all } = await this.adminActions.getAnnouncements({ + credentials: useOAuthStore().token, + }) + + const { data: visible } = await getAnnouncements({ + credentials: useOAuthStore().token, + }) + const visibleObject = visible.reduce((a, c) => { a[c.id] = c return a }, {}) + const getWithinVisible = (announcement) => visibleObject[announcement.id] @@ -56,35 +70,30 @@ export const useAnnouncementsStore = defineStore('announcements', { } }) - return all + this.announcements = all + } catch (error) { + // If and only if backend does not support announcements, it would return 404. + // In this case, silently ignores it. + if (error && error.statusCode === 404) { + this.supportsAnnouncements = false + } else { + throw error + } } - - return getAnnouncements() - .then((announcements) => { - this.announcements = announcements - }) - .catch((error) => { - // If and only if backend does not support announcements, it would return 404. - // In this case, silently ignores it. - if (error && error.statusCode === 404) { - this.supportsAnnouncements = false - } else { - throw error - } - }) }, markAnnouncementAsRead(id) { - return window.vuex.state.api.backendInteractor - .dismissAnnouncement({ id }) - .then(() => { - const index = this.announcements.findIndex((a) => a.id === id) + return dismissAnnouncement({ + id, + credentials: useOAuthStore().token, + }).then(() => { + const index = this.announcements.findIndex((a) => a.id === id) - if (index < 0) { - return - } + if (index < 0) { + return + } - this.announcements[index].read = true - }) + this.announcements[index].read = true + }) }, startFetchingAnnouncements() { if (this.fetchAnnouncementsTimer) { @@ -105,22 +114,38 @@ export const useAnnouncementsStore = defineStore('announcements', { clearInterval(interval) }, postAnnouncement({ content, startsAt, endsAt, allDay }) { - return window.vuex.state.api.backendInteractor - .postAnnouncement({ content, startsAt, endsAt, allDay }) + return this.adminActions + .postAnnouncement({ + credentials: useOAuthStore().token, + content, + startsAt, + endsAt, + allDay, + }) .then(() => { return this.fetchAnnouncements() }) }, editAnnouncement({ id, content, startsAt, endsAt, allDay }) { - return window.vuex.state.api.backendInteractor - .editAnnouncement({ id, content, startsAt, endsAt, allDay }) + return this.adminActions + .editAnnouncement({ + id, + content, + startsAt, + endsAt, + allDay, + credentials: useOAuthStore().token, + }) .then(() => { return this.fetchAnnouncements() }) }, deleteAnnouncement(id) { - return window.vuex.state.api.backendInteractor - .deleteAnnouncement({ id }) + return this.adminActions + .deleteAnnouncement({ + id, + credentials: useOAuthStore().token, + }) .then(() => { return this.fetchAnnouncements() }) diff --git a/src/stores/bookmark_folders.js b/src/stores/bookmark_folders.js index 028322f9d..713a21d00 100644 --- a/src/stores/bookmark_folders.js +++ b/src/stores/bookmark_folders.js @@ -1,6 +1,16 @@ import { find, remove } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + createBookmarkFolder, + deleteBookmarkFolder, + fetchBookmarkFolders, + updateBookmarkFolder, +} from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' + export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { state: () => ({ allFolders: [], @@ -16,6 +26,20 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { }, }, actions: { + startFetching() { + this.fetcher = promiseInterval(() => { + fetchBookmarkFolders({ + credentials: useOAuthStore().token, + }) + .then(({ data: folders }) => this.setBookmarkFolders(folders)) + .catch((e) => { + console.error(e) + }) + }, 240000) + }, + stopFetching() { + this.fetcher?.stop() + }, setBookmarkFolders(value) { this.allFolders = value }, @@ -30,23 +54,31 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { } }, createBookmarkFolder({ name, emoji }) { - return window.vuex.state.api.backendInteractor - .createBookmarkFolder({ name, emoji }) - .then((folder) => { - this.setBookmarkFolder(folder) - return folder - }) + return createBookmarkFolder({ + name, + emoji, + credentials: useOAuthStore().token, + }).then(({ data: folder }) => { + this.setBookmarkFolder(folder) + return folder + }) }, updateBookmarkFolder({ folderId, name, emoji }) { - return window.vuex.state.api.backendInteractor - .updateBookmarkFolder({ folderId, name, emoji }) - .then((folder) => { - this.setBookmarkFolder(folder) - return folder - }) + return updateBookmarkFolder({ + credentials: useOAuthStore().token, + folderId, + name, + emoji, + }).then(({ data: folder }) => { + this.setBookmarkFolder(folder) + return folder + }) }, deleteBookmarkFolder({ folderId }) { - window.vuex.state.api.backendInteractor.deleteBookmarkFolder({ folderId }) + deleteBookmarkFolder({ + folderId, + credentials: useOAuthStore().token, + }) remove(this.allFolders, (folder) => folder.id === folderId) }, }, diff --git a/src/stores/emoji.js b/src/stores/emoji.js index e28143300..3316f8328 100644 --- a/src/stores/emoji.js +++ b/src/stores/emoji.js @@ -2,7 +2,9 @@ import { merge } from 'lodash' import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { listEmojiPacks } from 'src/api/public.js' import { ensureFinalFallback } from 'src/i18n/languages.js' import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations' @@ -183,13 +185,14 @@ export const useEmojiStore = defineStore('emoji', { async getAdminPacksLocal(refresh) { if (!refresh && this.adminPacksLocal) return this.adminPacksLocal - const backendInteractor = window.vuex.state.api.backendInteractor - const listFunction = backendInteractor.listEmojiPacks - this.adminPacksLocalLoading = true this.adminPacksLocal = await this.getAdminPacks( useInstanceStore().server, - listFunction, + (params) => + listEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data), ) this.adminPacksLocalLoading = false }, @@ -206,12 +209,7 @@ export const useEmojiStore = defineStore('emoji', { page: 1, pageSize: 0, }) - .then((data) => data.json()) .then((data) => { - if (data.error !== undefined) { - return Promise.reject(data.error) - } - const promises = [] for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { @@ -220,15 +218,9 @@ export const useEmojiStore = defineStore('emoji', { instance, page: i, pageSize, - }) - .then((data) => data.json()) - .then((pageData) => { - if (pageData.error !== undefined) { - return Promise.reject(pageData.error) - } - - return pageData.packs - }), + }).then((pageData) => { + return pageData.packs + }), ) } @@ -247,7 +239,7 @@ export const useEmojiStore = defineStore('emoji', { }, {}) }) .catch((data) => { - this.displayError(data) + console.error(data) }) }, diff --git a/src/stores/instance.js b/src/stores/instance.js index 54b3cf43c..02edd1235 100644 --- a/src/stores/instance.js +++ b/src/stores/instance.js @@ -1,4 +1,4 @@ -import { get, set } from 'lodash' +import { set } from 'lodash' import { defineStore } from 'pinia' import { @@ -11,10 +11,11 @@ import { LOCAL_DEFAULT_CONFIG_DEFINITIONS, validateSetting, } from '../modules/default_config_state.js' -import apiService from '../services/api/api.service.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { fetchKnownDomains } from 'src/api/public.js' + const REMOTE_INTERACTION_URL = '/main/ostatus' const ROOT_STATE_DEFINITIONS = { @@ -210,9 +211,10 @@ export const useInstanceStore = defineStore('instance', { }, async getKnownDomains() { try { - this.knownDomains = await apiService.fetchKnownDomains({ + const { data } = await fetchKnownDomains({ credentials: window.vuex.state.users.currentUser.credentials, }) + this.knownDomains = data } catch (e) { console.warn("Can't load known domains\n", e) } diff --git a/src/stores/lists.js b/src/stores/lists.js index b33a119cc..37471d46e 100644 --- a/src/stores/lists.js +++ b/src/stores/lists.js @@ -1,8 +1,23 @@ import { find, remove } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + addAccountsToList, + createList, + deleteList, + fetchLists, + getList, + getListAccounts, + removeAccountsFromList, + updateList, +} from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' + export const useListsStore = defineStore('lists', { state: () => ({ + fetcher: null, allLists: [], allListsObject: {}, }), @@ -18,34 +33,57 @@ export const useListsStore = defineStore('lists', { }, }, actions: { + startFetching() { + this.fetcher = promiseInterval(() => { + fetchLists({ + credentials: useOAuthStore().token, + }) + .then(({ data: lists }) => this.setLists(lists)) + .catch((e) => { + console.error(e) + }) + }, 240000) + }, + stopFetching() { + this.fetcher?.stop() + }, setLists(value) { this.allLists = value }, - createList({ title }) { - return window.vuex.state.api.backendInteractor - .createList({ title }) - .then((list) => { - this.setList({ listId: list.id, title }) - return list - }) + async createList({ title }) { + return await createList({ + title, + credentials: useOAuthStore().token, + }).then(({ data: list }) => { + this.setList({ listId: list.id, title }) + return list + }) }, - fetchList({ listId }) { - return window.vuex.state.api.backendInteractor - .getList({ listId }) - .then((list) => this.setList({ listId: list.id, title: list.title })) + async fetchList({ listId }) { + return await getList({ + listId, + credentials: useOAuthStore().token, + }).then(({ data: list }) => + this.setList({ listId: list.id, title: list.title }), + ) }, - fetchListAccounts({ listId }) { - return window.vuex.state.api.backendInteractor - .getListAccounts({ listId }) - .then((accountIds) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - this.allListsObject[listId].accountIds = accountIds - }) + async fetchListAccounts({ listId }) { + return await getListAccounts({ + listId, + credentials: useOAuthStore().token, + }).then(({ data: accountIds }) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + }) }, - setList({ listId, title }) { - window.vuex.state.api.backendInteractor.updateList({ listId, title }) + async setList({ listId, title }) { + await updateList({ + listId, + title, + credentials: useOAuthStore().token, + }) if (!this.allListsObject[listId]) { this.allListsObject[listId] = { accountIds: [] } @@ -59,7 +97,7 @@ export const useListsStore = defineStore('lists', { entry.title = title } }, - setListAccounts({ listId, accountIds }) { + async setListAccounts({ listId, accountIds }) { const saved = this.allListsObject[listId]?.accountIds || [] const added = accountIds.filter((id) => !saved.includes(id)) const removed = saved.filter((id) => !accountIds.includes(id)) @@ -67,47 +105,62 @@ export const useListsStore = defineStore('lists', { this.allListsObject[listId] = { accountIds: [] } } this.allListsObject[listId].accountIds = accountIds + const promises = [] if (added.length > 0) { - window.vuex.state.api.backendInteractor.addAccountsToList({ - listId, - accountIds: added, - }) + promises.push( + addAccountsToList({ + listId, + accountIds: added, + credentials: useOAuthStore().token, + }), + ) } if (removed.length > 0) { - window.vuex.state.api.backendInteractor.removeAccountsFromList({ - listId, - accountIds: removed, - }) + promises.push( + removeAccountsFromList({ + listId, + accountIds: removed, + credentials: useOAuthStore().token, + }), + ) } + await Promise.all(promises) }, - addListAccount({ listId, accountId }) { - return window.vuex.state.api.backendInteractor - .addAccountsToList({ listId, accountIds: [accountId] }) - .then((result) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - this.allListsObject[listId].accountIds.push(accountId) - return result - }) + async addListAccount({ listId, accountId }) { + return await addAccountsToList({ + listId, + accountIds: [accountId], + credentials: useOAuthStore().token, + }).then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds.push(accountId) + return result + }) }, - removeListAccount({ listId, accountId }) { - return window.vuex.state.api.backendInteractor - .removeAccountsFromList({ listId, accountIds: [accountId] }) - .then((result) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - const { accountIds } = this.allListsObject[listId] - const set = new Set(accountIds) - set.delete(accountId) - this.allListsObject[listId].accountIds = [...set] + async removeListAccount({ listId, accountId }) { + return await removeAccountsFromList({ + listId, + accountIds: [accountId], + credentials: useOAuthStore().token, + }).then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = this.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + this.allListsObject[listId].accountIds = [...set] - return result - }) + return result + }) }, - deleteList({ listId }) { - window.vuex.state.api.backendInteractor.deleteList({ listId }) + async deleteList({ listId }) { + await deleteList({ + listId, + credentials: useOAuthStore().token, + }) delete this.allListsObject[listId] remove(this.allLists, (list) => list.id === listId) diff --git a/src/stores/local_config.js b/src/stores/local_config.js index 5b15c6ff1..786da657b 100644 --- a/src/stores/local_config.js +++ b/src/stores/local_config.js @@ -1,8 +1,5 @@ import { cloneDeep, set } from 'lodash' import { defineStore } from 'pinia' -import { toRaw } from 'vue' - -import { useInstanceStore } from 'src/stores/instance' import { LOCAL_DEFAULT_CONFIG, diff --git a/src/stores/oauth.js b/src/stores/oauth.js index 2a79c2fa9..35cd67afb 100644 --- a/src/stores/oauth.js +++ b/src/stores/oauth.js @@ -2,11 +2,7 @@ import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' -import { - createApp, - getClientToken, - verifyAppToken, -} from 'src/services/new_api/oauth.js' +import { createApp, getClientToken, verifyAppToken } from 'src/api/oauth.js' // status codes about verifyAppToken (GET /api/v1/apps/verify_credentials) const isAppTokenRejected = (error) => @@ -41,10 +37,7 @@ export const useOAuthStore = defineStore('oauth', { userToken: false, }), getters: { - getToken() { - return this.userToken || this.appToken - }, - getUserToken() { + token() { return this.userToken }, }, @@ -64,9 +57,9 @@ export const useOAuthStore = defineStore('oauth', { }, async createApp() { const instance = useInstanceStore().server - const app = await createApp(instance) - this.setClientData(app) - return app + const app = await createApp({ instance }) + this.setClientData(app.data) + return app.data }, /// Use this if you want to get the client id and secret but are not interested /// in whether they are valid. @@ -88,8 +81,8 @@ export const useOAuthStore = defineStore('oauth', { clientSecret: this.clientSecret, instance, }) - this.setAppToken(res.access_token) - return res.access_token + this.setAppToken(res.data.access_token) + return res.data.access_token }, /// Use this if you want to ensure the app is still valid to use. /// @return {string} The access token to the app (not attached to any user) @@ -97,8 +90,7 @@ export const useOAuthStore = defineStore('oauth', { if (this.appToken) { try { await verifyAppToken({ - instance: useInstanceStore().server, - appToken: this.appToken, + credentials: this.appToken, }) return this.appToken } catch (e) { diff --git a/src/stores/oauth_tokens.js b/src/stores/oauth_tokens.js index ae9b396ac..4d18ffae7 100644 --- a/src/stores/oauth_tokens.js +++ b/src/stores/oauth_tokens.js @@ -1,25 +1,35 @@ import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchOAuthTokens, revokeOAuthToken } from 'src/api/user.js' + +/* Just to clear the confusion: + * OAuth Store is responsible for user authentication + * OAuth Tokens Store is responsible for *managing* all of the user's tokens, + * i.e. for current and other clients + */ export const useOAuthTokensStore = defineStore('oauthTokens', { state: () => ({ tokens: [], }), actions: { fetchTokens() { - window.vuex.state.api.backendInteractor - .fetchOAuthTokens() - .then((tokens) => { - this.swapTokens(tokens) - }) + fetchOAuthTokens({ + credentials: useOAuthStore().token, + }).then(({ data: tokens }) => { + this.swapTokens(tokens) + }) }, revokeToken(id) { - window.vuex.state.api.backendInteractor - .revokeOAuthToken({ id }) - .then((response) => { - if (response.status === 201) { - this.swapTokens(this.tokens.filter((token) => token.id !== id)) - } - }) + revokeOAuthToken({ + id, + credentials: useOAuthStore().token, + }).then(({ status }) => { + if (status === 201) { + this.swapTokens(this.tokens.filter((token) => token.id !== id)) + } + }) }, swapTokens(tokens) { this.tokens = tokens diff --git a/src/stores/polls.js b/src/stores/polls.js index aac8a2421..e2f4b0ab2 100644 --- a/src/stores/polls.js +++ b/src/stores/polls.js @@ -1,6 +1,11 @@ import { merge } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchPoll } from 'src/api/public.js' +import { vote } from 'src/api/user.js' + export const usePollsStore = defineStore('polls', { state: () => ({ // Contains key = id, value = number of trackers for this poll @@ -19,16 +24,17 @@ export const usePollsStore = defineStore('polls', { } }, updateTrackedPoll(pollId) { - window.vuex.state.api.backendInteractor - .fetchPoll({ pollId }) - .then((poll) => { - setTimeout(() => { - if (this.trackedPolls[pollId]) { - this.updateTrackedPoll(pollId) - } - }, 30 * 1000) - this.mergeOrAddPoll(poll) - }) + fetchPoll({ + pollId, + credentials: useOAuthStore().token, + }).then(({ data: poll }) => { + setTimeout(() => { + if (this.trackedPolls[pollId]) { + this.updateTrackedPoll(pollId) + } + }, 30 * 1000) + this.mergeOrAddPoll(poll) + }) }, trackPoll(pollId) { if (!this.trackedPolls[pollId]) { @@ -50,12 +56,14 @@ export const usePollsStore = defineStore('polls', { } }, votePoll({ pollId, choices }) { - return window.vuex.state.api.backendInteractor - .vote({ pollId, choices }) - .then((poll) => { - this.mergeOrAddPoll(poll) - return poll - }) + return vote({ + pollId, + choices, + credentials: useOAuthStore().token, + }).then(({ data: poll }) => { + this.mergeOrAddPoll(poll) + return poll + }) }, }, }) diff --git a/src/stores/reports.js b/src/stores/reports.js index d3acebcb4..7d319d8e3 100644 --- a/src/stores/reports.js +++ b/src/stores/reports.js @@ -2,6 +2,9 @@ import { filter } from 'lodash' import { defineStore } from 'pinia' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { setReportState } from 'src/api/admin.js' export const useReportsStore = defineStore('reports', { state: () => ({ @@ -38,18 +41,21 @@ export const useReportsStore = defineStore('reports', { setReportState({ id, state }) { const oldState = this.reports[id].state this.reports[id].state = state - window.vuex.state.api.backendInteractor - .setReportState({ id, state }) - .catch((e) => { - console.error('Failed to set report state', e) - useInterfaceStore().pushGlobalNotice({ - level: 'error', - messageKey: 'general.generic_error_message', - messageArgs: [e.message], - timeout: 5000, - }) - this.reports[id].state = oldState + + setReportState({ + id, + state, + credentials: useOAuthStore().token, + }).catch((e) => { + console.error('Failed to set report state', e) + useInterfaceStore().pushGlobalNotice({ + level: 'error', + messageKey: 'general.generic_error_message', + messageArgs: [e.message], + timeout: 5000, }) + this.reports[id].state = oldState + }) }, addReport(report) { this.reports[report.id] = report diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 3010fc738..47c6978d5 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -20,9 +20,10 @@ import { toRaw } from 'vue' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useLocalConfigStore } from 'src/stores/local_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { updateProfileJSON } from 'src/api/user.js' import { storage } from 'src/lib/storage.js' import { makeUndefined, @@ -231,9 +232,17 @@ export const _mergeJournal = (...journals) => { Object.hasOwn(entry, 'timestamp'), ) const grouped = groupBy(allJournals, 'path') - const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => { - // side effect - journal.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)) + const trimmedGrouped = Object.entries(grouped).map(([path, rawJournal]) => { + const journal = rawJournal + .map((data, index) => ({ data, index })) + .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => { + if (a.timestamp === b.timestamp) { + return ai - bi + } else { + return a.timestamp > b.timestamp ? 1 : -1 + } + }) + .map((x) => x.data) if (path.startsWith('collections')) { const lastRemoveIndex = findLastIndex( @@ -268,9 +277,16 @@ export const _mergeJournal = (...journals) => { } }) - const flat = flatten(trimmedGrouped).sort((a, b) => - a.timestamp > b.timestamp ? 1 : -1, - ) + const flat = flatten(trimmedGrouped) + .map((data, index) => ({ data, index })) + .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => { + if (a.timestamp === b.timestamp) { + return ai - bi + } else { + return a.timestamp > b.timestamp ? 1 : -1 + } + }) + .map((x) => x.data) return take(flat, 500) } @@ -789,7 +805,10 @@ export const useSyncConfigStore = defineStore('sync_config', { if (!needPush) return this.updateCache({ username: window.vuex.state.users.currentUser.fqn }) const params = { pleroma_settings_store: { 'pleroma-fe': this.cache } } - window.vuex.state.api.backendInteractor.updateProfileJSON({ params }) + updateProfileJSON({ + params, + credentials: useOAuthStore().token, + }) }, }, persist: { diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js index f41c41628..759ebd509 100644 --- a/src/stores/user_highlight.js +++ b/src/stores/user_highlight.js @@ -1,19 +1,18 @@ import { merge as _merge, - clamp, clone, cloneDeep, - findLastIndex, flatten, - get, groupBy, isEqual, takeRight, - uniqWith, } from 'lodash' import { defineStore } from 'pinia' import { toRaw } from 'vue' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateProfileJSON } from 'src/api/user.js' import { storage } from 'src/lib/storage.js' export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically @@ -30,17 +29,6 @@ export const defaultState = { cache: null, } -export const _moveItemInArray = (array, value, movement) => { - const oldIndex = array.indexOf(value) - const newIndex = oldIndex + movement - const newArray = [...array] - // remove old - newArray.splice(oldIndex, 1) - // add new - newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value) - return newArray -} - const _wrapData = (data, userName) => { return { ...data, @@ -344,12 +332,13 @@ export const useUserHighlightStore = defineStore('user_highlight', { const params = { pleroma_settings_store: { user_highlight: this.cache }, } - window.vuex.state.api.backendInteractor - .updateProfileJSON({ params }) - .then((user) => { - this.initUserHighlight(user) - this.dirty = false - }) + updateProfileJSON({ + params, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + this.initUserHighlight(user) + this.dirty = false + }) }, }, persist: { diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 04747ee77..51a4de21e 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,13 @@ 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 --port $PORT --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: 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 diff --git a/test/fixtures/mock_api.js b/test/fixtures/mock_api.js index 03fb01a64..6fabe6356 100644 --- a/test/fixtures/mock_api.js +++ b/test/fixtures/mock_api.js @@ -1,73 +1,19 @@ -import { HttpResponse, http } from 'msw' -import { setupWorker } from 'msw/browser' import { test as testBase } from 'vitest' -// https://mswjs.io/docs/recipes/vitest-browser-mode -export const injectMswToTest = (defaultHandlers) => { - const worker = setupWorker(...defaultHandlers) +import { worker } from './worker.js' - return testBase.extend({ - worker: [ - // biome-ignore lint: required by vitest - async ({}, use) => { - await worker.start() +export const test = testBase.extend({ + worker: [ + // biome-ignore lint: required by vitest + async ({}, use) => { + await worker.start() - await use(worker) + await use(worker) - worker.resetHandlers() - worker.stop() - }, - { - auto: true, - }, - ], - }) -} - -export const testServer = 'https://test.server.example' - -export const authApis = [ - http.post(`${testServer}/api/v1/apps`, () => { - return HttpResponse.json({ - client_id: 'test-id', - client_secret: 'test-secret', - }) - }), - http.get(`${testServer}/api/v1/apps/verify_credentials`, ({ request }) => { - const authHeader = request.headers.get('Authorization') - if ( - authHeader === 'Bearer test-app-token' || - authHeader === 'Bearer also-good-app-token' - ) { - return HttpResponse.json({}) - } else { - // Pleroma 2.9.0 gives the following respoonse upon error - return HttpResponse.json( - { error: { detail: 'Internal server error' } }, - { - status: 400, - }, - ) - } - }), - http.post(`${testServer}/oauth/token`, async ({ request }) => { - const data = await request.formData() - - if ( - data.get('client_id') === 'test-id' && - data.get('client_secret') === 'test-secret' && - data.get('grant_type') === 'client_credentials' && - data.has('redirect_uri') - ) { - return HttpResponse.json({ access_token: 'test-app-token' }) - } else { - // Pleroma 2.9.0 gives the following respoonse upon error - return HttpResponse.json( - { error: 'Invalid credentials' }, - { - status: 400, - }, - ) - } - }), -] + worker.resetHandlers() + }, + { + auto: true, + }, + ], +}) diff --git a/test/fixtures/worker.js b/test/fixtures/worker.js new file mode 100644 index 000000000..e6ed89dc9 --- /dev/null +++ b/test/fixtures/worker.js @@ -0,0 +1,5 @@ +import { setupWorker } from 'msw/browser' + +export const worker = setupWorker() + +window.__test__ = window.__test__ || 'TEST' diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js index 8b198420b..36d323575 100644 --- a/test/unit/specs/components/user_profile.spec.js +++ b/test/unit/specs/components/user_profile.spec.js @@ -4,7 +4,6 @@ import { createStore } from 'vuex' import UserProfile from 'src/components/user_profile/user_profile.vue' import { getters } from 'src/modules/users.js' -import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js' const mutations = { clearTimeline: () => { @@ -53,10 +52,6 @@ const externalProfileStore = createStore({ actions, getters: testGetters, state: { - api: { - fetchers: {}, - backendInteractor: backendInteractorService(''), - }, interface: { browserSupport: '', }, @@ -116,10 +111,6 @@ const localProfileStore = createStore({ actions, getters: testGetters, state: { - api: { - fetchers: {}, - backendInteractor: backendInteractorService(''), - }, interface: { browserSupport: '', }, diff --git a/test/unit/specs/services/api/helpers.spec.js b/test/unit/specs/services/api/helpers.spec.js new file mode 100644 index 000000000..11874790a --- /dev/null +++ b/test/unit/specs/services/api/helpers.spec.js @@ -0,0 +1,90 @@ +import { paramsString } from 'src/api/helpers.js' + +describe('API Helpers', () => { + describe('paramsString', () => { + it('should return empty string when given empty object', () => { + const string = paramsString({}) + + expect(string).to.eq('') + }) + + it('should return empty string when given null', () => { + const string = paramsString(null) + + expect(string).to.eq('') + }) + + it('should return empty string when given undefined', () => { + const string = paramsString(undefined) + + expect(string).to.eq('') + }) + + it('should return URI param string for normal object', () => { + const string = paramsString({ a: 1, b: '3' }) + + expect(string).to.eq('?a=1&b=3') + }) + + it('should encode objects correctly', () => { + const string = paramsString({ foo: true, bar: [1, 2, 3] }) + + expect(string).to.eq('?foo=true&bar[]=1&bar[]=2&bar[]=3') + }) + + it('should drop nullish params', () => { + const string = paramsString({ present: 'yes', missing: null }) + + expect(string).to.eq('?present=yes') + }) + + it('should convert camelCase keys to snake_keys objects correctly', () => { + const string = paramsString({ isActive: true, MaybeNot: false }) + + expect(string).to.eq('?is_active=true&maybe_not=false') + }) + + it('should work with maps', () => { + const string = paramsString( + new Map([ + ['key', 'yes'], + ['key2', 'also yes'], + ]), + ) + + expect(string).to.eq('?key=yes&key_2=also%20yes') + }) + + it('should escape components correctly', () => { + const string = paramsString({ gachi: '♂', muchi: 'Билли Геррингтон' }) + + expect(string).to.eq( + '?gachi=%E2%99%82&muchi=%D0%91%D0%B8%D0%BB%D0%BB%D0%B8%20%D0%93%D0%B5%D1%80%D1%80%D0%B8%D0%BD%D0%B3%D1%82%D0%BE%D0%BD', + ) + }) + + it('should throw when passed a non-object', () => { + expect(() => { + paramsString('Totally an object') + }).to.throw() + }) + + it('should throw when passed an array', () => { + expect(() => { + paramsString(['Totally an object']) + }).to.throw() + }) + + it('should throw when array param is non-primitive', () => { + expect(() => { + paramsString({ a: [() => ''] }) + }).to.throw() + }) + + it('should throw when array param is nullish', () => { + expect(() => { + paramsString({ a: [1, null, 3] }) + }).to.throw() + }) + }) +}) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 43ce4da5d..afd17e56b 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,10 +1,10 @@ +import mastoapidata from '../../../../fixtures/mastoapi.json' + import { parseLinkHeaderPagination, - parseNotification, parseStatus, parseUser, -} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' -import mastoapidata from '../../../../fixtures/mastoapi.json' +} from 'src/services/entity_normalizer/entity_normalizer.service.js' const makeMockUserMasto = (overrides = {}) => { return Object.assign( diff --git a/test/unit/specs/stores/lists.spec.js b/test/unit/specs/stores/lists.spec.js index 083730856..bb1ef12b4 100644 --- a/test/unit/specs/stores/lists.spec.js +++ b/test/unit/specs/stores/lists.spec.js @@ -1,22 +1,23 @@ -import { createPinia, setActivePinia } from 'pinia' -import { createStore } from 'vuex' +import { createTestingPinia } from '@pinia/testing' +import { HttpResponse, http } from 'msw' +import { setActivePinia } from 'pinia' + +import { test as it } from '/test/fixtures/mock_api.js' import { useListsStore } from 'src/stores/lists.js' -import apiModule from 'src/modules/api.js' - -setActivePinia(createPinia()) -const store = useListsStore() -window.vuex = createStore({ - modules: { - api: apiModule, - }, -}) +import { MASTODON_LIST_ACCOUNTS_URL, MASTODON_LIST_URL } from 'src/api/user.js' describe('The lists store', () => { + let store + + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + store = useListsStore() + }) + describe('actions', () => { it('updates array of all lists', () => { - store.$reset() const list = { id: '1', title: 'testList' } store.setLists([list]) @@ -24,12 +25,19 @@ describe('The lists store', () => { expect(store.allLists).to.eql([list]) }) - it('adds a new list with a title, updating the title for existing lists', () => { - store.$reset() + it('adds a new list with a title, updating the title for existing lists', async ({ + worker, + }) => { const list = { id: '1', title: 'testList' } const modList = { id: '1', title: 'anotherTestTitle' } - store.setList({ listId: list.id, title: list.title }) + worker.use( + http.put(MASTODON_LIST_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.setList({ listId: list.id, title: list.title }) expect(store.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [], @@ -37,7 +45,7 @@ describe('The lists store', () => { expect(store.allLists).to.have.length(1) expect(store.allLists[0]).to.eql(list) - store.setList({ listId: modList.id, title: modList.title }) + await store.setList({ listId: modList.id, title: modList.title }) expect(store.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [], @@ -46,24 +54,38 @@ describe('The lists store', () => { expect(store.allLists[0]).to.eql(modList) }) - it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { - store.$reset() + it('adds a new list with an array of IDs, updating the IDs for existing lists', async ({ + worker, + }) => { const list = { id: '1', accountIds: ['1', '2', '3'] } const modList = { id: '1', accountIds: ['3', '4', '5'] } - store.setListAccounts({ listId: list.id, accountIds: list.accountIds }) + worker.use( + http.post(MASTODON_LIST_ACCOUNTS_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + http.delete(MASTODON_LIST_ACCOUNTS_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.setListAccounts({ + listId: list.id, + accountIds: list.accountIds, + }) expect(store.allListsObject[list.id].accountIds).to.eql(list.accountIds) - store.setListAccounts({ + await store.setListAccounts({ listId: modList.id, accountIds: modList.accountIds, }) + expect(store.allListsObject[modList.id].accountIds).to.eql( modList.accountIds, ) }) - it('deletes a list', () => { + it('deletes a list', async ({ worker }) => { store.$patch({ allLists: [{ id: '1', title: 'testList' }], allListsObject: { @@ -72,7 +94,13 @@ describe('The lists store', () => { }) const listId = '1' - store.deleteList({ listId }) + worker.use( + http.delete(MASTODON_LIST_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.deleteList({ listId }) expect(store.allLists).to.have.length(0) expect(store.allListsObject).to.eql({}) }) diff --git a/test/unit/specs/stores/oauth.spec.js b/test/unit/specs/stores/oauth.spec.js index 4664bba02..a06cbb2fd 100644 --- a/test/unit/specs/stores/oauth.spec.js +++ b/test/unit/specs/stores/oauth.spec.js @@ -1,27 +1,80 @@ import { createTestingPinia } from '@pinia/testing' import { HttpResponse, http } from 'msw' -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' -import { - authApis, - injectMswToTest, - testServer, -} from '/test/fixtures/mock_api.js' +import { test as it } from '/test/fixtures/mock_api.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' -const test = injectMswToTest(authApis) +import { + MASTODON_APP_URL, + MASTODON_APP_VERIFY_URL, + OAUTH_TOKEN_URL, +} from 'src/api/oauth.js' + +const authApis = () => [ + http.post(MASTODON_APP_URL, () => { + return HttpResponse.json({ + client_id: 'test-id', + client_secret: 'test-secret', + }) + }), + http.get(MASTODON_APP_VERIFY_URL, ({ request }) => { + const authHeader = request.headers.get('Authorization') + if ( + authHeader === 'Bearer test-app-token' || + authHeader === 'Bearer also-good-app-token' + ) { + return HttpResponse.json({}) + } else { + // Pleroma 2.9.0 gives the following respoonse upon error + return HttpResponse.json( + { error: { detail: 'Internal server error' } }, + { + status: 400, + }, + ) + } + }), + http.post(OAUTH_TOKEN_URL, async ({ request }) => { + const data = await request.formData() + + if ( + data.get('client_id') === 'test-id' && + data.get('client_secret') === 'test-secret' && + data.get('grant_type') === 'client_credentials' && + data.has('redirect_uri') + ) { + return HttpResponse.json({ access_token: 'test-app-token' }) + } else { + // Pleroma 2.9.0 gives the following respoonse upon error + return HttpResponse.json( + { error: 'Invalid credentials' }, + { + status: 400, + }, + ) + } + }), +] describe('oauth store', () => { beforeEach(() => { setActivePinia(createTestingPinia({ stubActions: false })) - useInstanceStore().server = testServer }) describe('createApp', () => { - test('it should use create an app and record client id and secret', async () => { + 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() expect(store.clientId).to.eql('test-id') expect(store.clientSecret).to.eql('test-secret') @@ -29,9 +82,9 @@ describe('oauth store', () => { expect(app.clientSecret).to.eql('test-secret') }) - test('it should throw and not update if failed', async ({ worker }) => { + it('should throw and not update if failed', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) @@ -45,7 +98,8 @@ describe('oauth store', () => { }) describe('ensureApp', () => { - test('it should create an app if it does not exist', async () => { + it('should create an app if it does not exist', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() const app = await store.ensureApp() expect(store.clientId).to.eql('test-id') @@ -54,9 +108,9 @@ describe('oauth store', () => { expect(app.clientSecret).to.eql('test-secret') }) - test('it should not create an app if it exists', async ({ worker }) => { + it('should not create an app if it exists', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -74,7 +128,8 @@ describe('oauth store', () => { }) describe('getAppToken', () => { - test('it should get app token and set it in state', async () => { + it('should get app token and set it in state', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'test-id' store.clientSecret = 'test-secret' @@ -84,7 +139,10 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should throw and not set state if it cannot get app token', async () => { + it('should throw and not set state if it cannot get app token', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'bad-id' store.clientSecret = 'bad-secret' @@ -95,14 +153,16 @@ describe('oauth store', () => { }) describe('ensureAppToken', () => { - test('it should work if the state is empty', async () => { + it('should work if the state is empty', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() const token = await store.ensureAppToken() expect(token).to.eql('test-app-token') expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we already have a working token', async () => { + it('should work if we already have a working token', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() store.appToken = 'also-good-app-token' @@ -111,11 +171,12 @@ describe('oauth store', () => { expect(store.appToken).to.eql('also-good-app-token') }) - test('it should work if we have a bad token but good app credentials', async ({ + it('should work if we have a bad token but good app credentials', async ({ worker, }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + ...authApis(), + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -129,11 +190,12 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we have no token but good app credentials', async ({ + it('should work if we have no token but good app credentials', async ({ worker, }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + ...authApis(), + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -146,7 +208,10 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we have no token and bad app credentials', async () => { + it('should work if we have no token and bad app credentials', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'bad-id' store.clientSecret = 'bad-secret' @@ -158,7 +223,10 @@ describe('oauth store', () => { expect(store.clientSecret).to.eql('test-secret') }) - test('it should work if we have bad token and bad app credentials', async () => { + it('should work if we have bad token and bad app credentials', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.appToken = 'bad-app-token' store.clientId = 'bad-id' @@ -171,9 +239,9 @@ describe('oauth store', () => { expect(store.clientSecret).to.eql('test-secret') }) - test('it should throw if we cannot create an app', async ({ worker }) => { + it('should throw if we cannot create an app', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) @@ -182,17 +250,15 @@ describe('oauth store', () => { await expect(store.ensureAppToken()).rejects.toThrowError('Throttled') }) - test('it should throw if we cannot obtain app token', async ({ - worker, - }) => { + it('should throw if we cannot obtain app token', async ({ worker }) => { worker.use( - http.post(`${testServer}/oauth/token`, () => { + http.post(OAUTH_TOKEN_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) const store = useOAuthStore() - await expect(store.ensureAppToken()).rejects.toThrowError('Throttled') + await expect(store.getAppToken()).rejects.toThrowError('Throttled') }) }) }) diff --git a/test/unit/specs/stores/user_highlight.spec.js b/test/unit/specs/stores/user_highlight.spec.js index c46852256..f80143b6e 100644 --- a/test/unit/specs/stores/user_highlight.spec.js +++ b/test/unit/specs/stores/user_highlight.spec.js @@ -1,10 +1,8 @@ -import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { _getRecentData, _mergeHighlights, - _moveItemInArray, useUserHighlightStore, } from 'src/stores/user_highlight.js' diff --git a/vite.config.js b/vite.config.js index 5b7c22d57..beff5fe76 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,8 +1,8 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { DevTools } from '@vitejs/devtools' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' +import { playwright } from '@vitest/browser-playwright' import { defineConfig } from 'vite' import eslint from 'vite-plugin-eslint2' import stylelint from 'vite-plugin-stylelint' @@ -72,6 +72,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': { @@ -79,6 +80,14 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', ws: true, + rewriteWsOrigin: true, + }, + '/auth': { + // Mastodon password reset lives here + target, + changeOrigin: true, + cookieDomainRewrite: 'localhost', + ws: true, }, '/nodeinfo': { target, @@ -90,20 +99,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' @@ -233,8 +243,10 @@ export default defineConfig(async ({ mode, command }) => { exclude: [...configDefaults.exclude, 'test/e2e-playwright/**'], browser: { enabled: true, - provider: 'playwright', - instances: [{ browser: 'firefox' }], + headless: true, + provider: playwright(), + // https://github.com/mswjs/msw/issues/2757 + instances: [{ browser: 'chromium' }], }, }, } diff --git a/yarn.lock b/yarn.lock index 4f3f12589..b60f88b7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,7 +29,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== @@ -1131,11 +1131,6 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== -"@babel/runtime@^7.12.5": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" - integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== - "@babel/template@^7.27.0": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" @@ -1327,33 +1322,16 @@ resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz#71ba2fb5505b3b01dd3cf551ef329e0094636125" integrity sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg== +"@blazediff/core@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@blazediff/core/-/core-1.9.1.tgz#ad61c4ec48dc11a2913b9753c8c74902e05e8f14" + integrity sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA== + "@bufbuild/protobuf@^2.5.0": version "2.12.0" resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.12.0.tgz#53225636a8fcebb2bd94998ad9d42f99f96add4d" integrity sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA== -"@bundled-es-modules/cookie@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507" - integrity sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw== - dependencies: - cookie "^0.7.2" - -"@bundled-es-modules/statuses@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872" - integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg== - dependencies: - statuses "^2.0.1" - -"@bundled-es-modules/tough-cookie@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3" - integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw== - dependencies: - "@types/tough-cookie" "^4.0.5" - tough-cookie "^4.1.4" - "@cacheable/memoize@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@cacheable/memoize/-/memoize-2.0.3.tgz#64b18a6b42f987fe8a9e9e2e4391b14cbf85680f" @@ -1572,136 +1550,6 @@ dependencies: tslib "^2.4.0" -"@esbuild/aix-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49" - integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg== - -"@esbuild/android-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03" - integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ== - -"@esbuild/android-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae" - integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg== - -"@esbuild/android-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6" - integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g== - -"@esbuild/darwin-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84" - integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w== - -"@esbuild/darwin-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe" - integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ== - -"@esbuild/freebsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a" - integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA== - -"@esbuild/freebsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb" - integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw== - -"@esbuild/linux-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5" - integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA== - -"@esbuild/linux-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f" - integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw== - -"@esbuild/linux-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b" - integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw== - -"@esbuild/linux-loong64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb" - integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw== - -"@esbuild/linux-mips64el@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5" - integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ== - -"@esbuild/linux-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74" - integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw== - -"@esbuild/linux-riscv64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273" - integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww== - -"@esbuild/linux-s390x@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263" - integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw== - -"@esbuild/linux-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910" - integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ== - -"@esbuild/netbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077" - integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg== - -"@esbuild/netbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034" - integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A== - -"@esbuild/openbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad" - integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg== - -"@esbuild/openbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2" - integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw== - -"@esbuild/openharmony-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1" - integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ== - -"@esbuild/sunos-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244" - integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA== - -"@esbuild/win32-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935" - integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q== - -"@esbuild/win32-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343" - integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA== - -"@esbuild/win32-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f" - integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA== - "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0": version "4.5.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" @@ -1856,37 +1704,41 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== -"@inquirer/confirm@^5.0.0": - version "5.1.8" - resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.8.tgz#476af2476cd4867905dcabfca8598da4dd65e923" - integrity sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg== - dependencies: - "@inquirer/core" "^10.1.9" - "@inquirer/type" "^3.0.5" +"@inquirer/ansi@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4" + integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q== -"@inquirer/core@^10.1.9": - version "10.1.9" - resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.9.tgz#9ab672a2d9ca60c5d45c7fa9b63e4fe9e038a02e" - integrity sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw== +"@inquirer/confirm@^6.0.11": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.1.1.tgz#9c6a7d79c6132b2af57fdb75747f056204e55356" + integrity sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ== dependencies: - "@inquirer/figures" "^1.0.11" - "@inquirer/type" "^3.0.5" - ansi-escapes "^4.3.2" + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/core@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-11.2.1.tgz#54ccd8f7d47852140b6066cbd77d63b2c2b168fd" + integrity sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" cli-width "^4.1.0" - mute-stream "^2.0.0" + fast-wrap-ansi "^0.2.0" + mute-stream "^3.0.0" signal-exit "^4.1.0" - wrap-ansi "^6.2.0" - yoctocolors-cjs "^2.1.2" -"@inquirer/figures@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.11.tgz#4744e6db95288fea1dead779554859710a959a21" - integrity sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw== +"@inquirer/figures@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.7.tgz#f5cc5843732a81304d06a0db4b53cc7dbda15541" + integrity sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw== -"@inquirer/type@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.5.tgz#fe00207e57d5f040e5b18e809c8e7abc3a2ade3a" - integrity sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg== +"@inquirer/type@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.0.7.tgz#9c6f0d857fe6ad549a3a932343b64e76acb34b10" + integrity sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g== "@intlify/core-base@11.1.12": version "11.1.12" @@ -1979,10 +1831,10 @@ resolved "https://registry.yarnpkg.com/@keyv/serialize/-/serialize-1.1.1.tgz#0c01dd3a3483882af7cf3878d4e71d505c81fc4a" integrity sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA== -"@mswjs/interceptors@^0.39.1": - version "0.39.2" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.39.2.tgz#de9de0ab23f99d387c7904df7219a92157d1d666" - integrity sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg== +"@mswjs/interceptors@^0.41.3": + version "0.41.9" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz#9d90bbd60d1ddc30dbcbb827a9bb2e470493530d" + integrity sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -2162,6 +2014,11 @@ resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== +"@open-draft/deferred-promise@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz#9725acc5afe8ecde690e9e198a094859fdbf2e45" + integrity sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA== + "@open-draft/logger@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" @@ -2170,7 +2027,7 @@ is-node-process "^1.2.0" outvariant "^1.4.0" -"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0": +"@open-draft/until@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== @@ -2503,116 +2360,6 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db" - integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ== - -"@rollup/rollup-android-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5" - integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA== - -"@rollup/rollup-darwin-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz#8a102869c88f3780c7d5e6776afd3f19084ecd7f" - integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA== - -"@rollup/rollup-darwin-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956" - integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA== - -"@rollup/rollup-freebsd-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899" - integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA== - -"@rollup/rollup-freebsd-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10" - integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c" - integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ== - -"@rollup/rollup-linux-arm-musleabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00" - integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ== - -"@rollup/rollup-linux-arm64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc" - integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg== - -"@rollup/rollup-linux-arm64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0" - integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q== - -"@rollup/rollup-linux-loong64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2" - integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA== - -"@rollup/rollup-linux-ppc64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5" - integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw== - -"@rollup/rollup-linux-riscv64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994" - integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw== - -"@rollup/rollup-linux-riscv64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f" - integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg== - -"@rollup/rollup-linux-s390x-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b" - integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ== - -"@rollup/rollup-linux-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278" - integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q== - -"@rollup/rollup-linux-x64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350" - integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg== - -"@rollup/rollup-openharmony-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30" - integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw== - -"@rollup/rollup-win32-arm64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937" - integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w== - -"@rollup/rollup-win32-ia32-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50" - integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg== - -"@rollup/rollup-win32-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3" - integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ== - -"@rollup/rollup-win32-x64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107" - integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg== - "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -2646,30 +2393,16 @@ lodash.get "^4.4.2" type-detect "^4.1.0" +"@standard-schema/spec@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + "@testim/chrome-version@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz#86e04e677cd6c05fa230dd15ac223fa72d1d7090" integrity sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g== -"@testing-library/dom@^10.4.0": - version "10.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" - integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.3.0" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - -"@testing-library/user-event@^14.6.1": - version "14.6.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" - integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== - "@tootallnate/quickjs-emscripten@^0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" @@ -2682,22 +2415,25 @@ dependencies: tslib "^2.4.0" -"@types/aria-query@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" - integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== - "@types/chai@^4.3.5": version "4.3.20" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== -"@types/cookie@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" - integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" -"@types/estree@1.0.8", "@types/estree@^1.0.0": +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/estree@^1.0.0": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -2739,15 +2475,17 @@ "@types/node" "*" "@types/ws" "*" -"@types/statuses@^2.0.4": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63" - integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A== +"@types/set-cookie-parser@^2.4.10": + version "2.4.10" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz#ad3a807d6d921db9720621ea3374c5d92020bcbc" + integrity sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw== + dependencies: + "@types/node" "*" -"@types/tough-cookie@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" - integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/statuses@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" + integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== "@types/ws@*": version "8.18.1" @@ -2853,91 +2591,101 @@ dependencies: "@rolldown/pluginutils" "^1.0.1" -"@vitest/browser@^3.0.7": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-3.1.3.tgz#985f12382bc4aeddbffa4209850ab5cbaaa43e60" - integrity sha512-Dgyez9LbHJHl9ObZPo5mu4zohWLo7SMv8zRWclMF+dxhQjmOtEP0raEX13ac5ygcvihNoQPBZXdya5LMSbcCDQ== +"@vitest/browser-playwright@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/browser-playwright/-/browser-playwright-4.1.9.tgz#845e65017dfed8aff59931f91016e7595b8f5c1d" + integrity sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw== dependencies: - "@testing-library/dom" "^10.4.0" - "@testing-library/user-event" "^14.6.1" - "@vitest/mocker" "3.1.3" - "@vitest/utils" "3.1.3" - magic-string "^0.30.17" - sirv "^3.0.1" - tinyrainbow "^2.0.0" - ws "^8.18.1" + "@vitest/browser" "4.1.9" + "@vitest/mocker" "4.1.9" + tinyrainbow "^3.1.0" -"@vitest/expect@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.1.3.tgz#bbca175cd2f23d7de9448a215baed8f3d7abd7b7" - integrity sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg== +"@vitest/browser@4.1.9", "@vitest/browser@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-4.1.9.tgz#838c5f215f4015089979cf49f930cd3db2888461" + integrity sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg== dependencies: - "@vitest/spy" "3.1.3" - "@vitest/utils" "3.1.3" - chai "^5.2.0" - tinyrainbow "^2.0.0" + "@blazediff/core" "1.9.1" + "@vitest/mocker" "4.1.9" + "@vitest/utils" "4.1.9" + magic-string "^0.30.21" + pngjs "^7.0.0" + sirv "^3.0.2" + tinyrainbow "^3.1.0" + ws "^8.19.0" -"@vitest/mocker@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.1.3.tgz#121d0f2fcca20c9ccada9e2d6e761f7fc687f4ce" - integrity sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ== +"@vitest/expect@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.9.tgz#ba1af73ae53262e3dc9b518cb7b76fb614e0ef53" + integrity sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA== dependencies: - "@vitest/spy" "3.1.3" + "@standard-schema/spec" "^1.1.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + chai "^6.2.2" + tinyrainbow "^3.1.0" + +"@vitest/mocker@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.9.tgz#a483de79b358aba3dd8f319a0d8ab17c89f5c75d" + integrity sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw== + dependencies: + "@vitest/spy" "4.1.9" estree-walker "^3.0.3" - magic-string "^0.30.17" + magic-string "^0.30.21" -"@vitest/pretty-format@3.1.3", "@vitest/pretty-format@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.1.3.tgz#760b9eab5f253d7d2e7dcd28ef34570f584023d4" - integrity sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA== +"@vitest/pretty-format@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz#885cfe9fcb6ff3df4409ea66192cc1fb23d62fae" + integrity sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A== dependencies: - tinyrainbow "^2.0.0" + tinyrainbow "^3.1.0" -"@vitest/runner@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.1.3.tgz#b268fa90fca38fab363f1107f057c0a2a141ee45" - integrity sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA== +"@vitest/runner@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.9.tgz#bb742947ce4841dfb2d8984a2f9014850be10f51" + integrity sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg== dependencies: - "@vitest/utils" "3.1.3" + "@vitest/utils" "4.1.9" pathe "^2.0.3" -"@vitest/snapshot@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.1.3.tgz#39a8f9f8c6ba732ffde59adeacf0a549bef11e76" - integrity sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ== +"@vitest/snapshot@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.9.tgz#bdfb670ae5617613ea8776e93d0666a66defeeb7" + integrity sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA== dependencies: - "@vitest/pretty-format" "3.1.3" - magic-string "^0.30.17" + "@vitest/pretty-format" "4.1.9" + "@vitest/utils" "4.1.9" + magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.1.3.tgz#ca81e2b4f0c3d6c75f35defa77c3336f39c8f605" - integrity sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ== - dependencies: - tinyspy "^3.0.2" +"@vitest/spy@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.9.tgz#bfc40d48fb9bd1a1228bfbfde7f5555e7f6b3867" + integrity sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA== -"@vitest/ui@^3.0.7": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-3.1.3.tgz#ad3c3160e6c86d79f817e09b1f8f02f0e2799851" - integrity sha512-IipSzX+8DptUdXN/GWq3hq5z18MwnpphYdOMm0WndkRGYELzfq7NDP8dMpZT7JGW1uXFrIGxOW2D0Xi++ulByg== +"@vitest/ui@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-4.1.9.tgz#8aeb5af7295ea04ef1064873334ced04ce20d646" + integrity sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ== dependencies: - "@vitest/utils" "3.1.3" + "@vitest/utils" "4.1.9" fflate "^0.8.2" - flatted "^3.3.3" + flatted "^3.4.2" pathe "^2.0.3" - sirv "^3.0.1" - tinyglobby "^0.2.13" - tinyrainbow "^2.0.0" + sirv "^3.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" -"@vitest/utils@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.1.3.tgz#4f31bdfd646cd82d30bfa730d7410cb59d529669" - integrity sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg== +"@vitest/utils@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.9.tgz#0184c7e6eb3234739b2b6b3b985f78d1ed823ee1" + integrity sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA== dependencies: - "@vitest/pretty-format" "3.1.3" - loupe "^3.1.3" - tinyrainbow "^2.0.0" + "@vitest/pretty-format" "4.1.9" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" "@vue/babel-helper-vue-jsx-merge-props@1.4.0": version "1.4.0" @@ -3342,13 +3090,6 @@ ansi-colors@4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" @@ -3378,11 +3119,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -3460,13 +3196,6 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" -aria-query@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -3808,11 +3537,6 @@ bytes@3.1.2, bytes@^3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== - cac@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cac/-/cac-7.0.0.tgz#7dda83da2268f75f840ab89ac3bcc36c120a78da" @@ -3903,16 +3627,10 @@ chai@5.3.3: loupe "^3.1.0" pathval "^2.0.0" -chai@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05" - integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw== - dependencies: - assertion-error "^2.0.1" - check-error "^2.1.1" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" +chai@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== chalk@2.4.2, chalk@^2.4.2: version "2.4.2" @@ -4184,11 +3902,16 @@ cookie-signature@^1.2.1: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1, cookie@^0.7.2: +cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + copy-anything@^3.0.2: version "3.0.5" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" @@ -4500,11 +4223,6 @@ depd@2.0.0, depd@^2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - destr@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" @@ -4574,11 +4292,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.9: - version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" - integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -4864,10 +4577,10 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-module-lexer@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" - integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== +es-module-lexer@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c" + integrity sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -4902,38 +4615,6 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild@^0.25.0: - version "0.25.11" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d" - integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q== - optionalDependencies: - "@esbuild/aix-ppc64" "0.25.11" - "@esbuild/android-arm" "0.25.11" - "@esbuild/android-arm64" "0.25.11" - "@esbuild/android-x64" "0.25.11" - "@esbuild/darwin-arm64" "0.25.11" - "@esbuild/darwin-x64" "0.25.11" - "@esbuild/freebsd-arm64" "0.25.11" - "@esbuild/freebsd-x64" "0.25.11" - "@esbuild/linux-arm" "0.25.11" - "@esbuild/linux-arm64" "0.25.11" - "@esbuild/linux-ia32" "0.25.11" - "@esbuild/linux-loong64" "0.25.11" - "@esbuild/linux-mips64el" "0.25.11" - "@esbuild/linux-ppc64" "0.25.11" - "@esbuild/linux-riscv64" "0.25.11" - "@esbuild/linux-s390x" "0.25.11" - "@esbuild/linux-x64" "0.25.11" - "@esbuild/netbsd-arm64" "0.25.11" - "@esbuild/netbsd-x64" "0.25.11" - "@esbuild/openbsd-arm64" "0.25.11" - "@esbuild/openbsd-x64" "0.25.11" - "@esbuild/openharmony-arm64" "0.25.11" - "@esbuild/sunos-x64" "0.25.11" - "@esbuild/win32-arm64" "0.25.11" - "@esbuild/win32-ia32" "0.25.11" - "@esbuild/win32-x64" "0.25.11" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -5250,10 +4931,10 @@ execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -expect-type@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f" - integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw== +expect-type@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== express@5.1.0: version "5.1.0" @@ -5330,11 +5011,30 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-string-truncated-width@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz#23afe0da67d752ca0727538f1e6967759728ce49" + integrity sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g== + +fast-string-width@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-string-width/-/fast-string-width-3.0.2.tgz#16dbabb491ce5585b5ecb675b65c165d71688eeb" + integrity sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg== + dependencies: + fast-string-truncated-width "^3.0.2" + fast-uri@^3.0.1: version "3.0.6" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== +fast-wrap-ansi@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz#95e952a0145bce3f59ad56e179f84c48d4072935" + integrity sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q== + dependencies: + fast-string-width "^3.0.2" + fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -5354,7 +5054,7 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.4.4, fdir@^6.5.0: +fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== @@ -5463,6 +5163,11 @@ flatted@^3.2.9, flatted@^3.3.3: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +flatted@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== + follow-redirects@^1.0.0, follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -5756,10 +5461,10 @@ graceful-fs@^4.2.0, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphql@^16.8.1: - version "16.10.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" - integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== +graphql@^16.13.2: + version "16.14.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.2.tgz#83faf25869e3df727cc855161db5da85b0e5b2c0" + integrity sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA== h3@2.0.1-rc.22: version "2.0.1-rc.22" @@ -5842,10 +5547,13 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-polyfill@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" - integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== +headers-polyfill@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-5.0.1.tgz#9554eb2892b666db1c7a3380a91b6cfd467a6b19" + integrity sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA== + dependencies: + "@types/set-cookie-parser" "^2.4.10" + set-cookie-parser "^3.0.1" hookable@^5.5.3: version "5.5.3" @@ -6714,7 +6422,7 @@ loupe@^2.3.7: dependencies: get-func-name "^2.0.1" -loupe@^3.1.0, loupe@^3.1.3: +loupe@^3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== @@ -6748,11 +6456,6 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -lz-string@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" - integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== - magic-string@^0.30.17: version "0.30.18" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.18.tgz#905bfbbc6aa5692703a93db26a9edcaa0007d2bb" @@ -6980,34 +6683,34 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@2.10.5: - version "2.10.5" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.10.5.tgz#3e43f12e97581c260bf38d8817732b9fec3bfdb0" - integrity sha512-0EsQCrCI1HbhpBWd89DvmxY6plmvrM96b0sCIztnvcNHQbXn5vqwm1KlXslo6u4wN9LFGLC1WFjjgljcQhe40A== +msw@2.14.6: + version "2.14.6" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.14.6.tgz#d30fa6ce8ec3299c6d9bf644cee3a5cc3c3f1197" + integrity sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg== dependencies: - "@bundled-es-modules/cookie" "^2.0.1" - "@bundled-es-modules/statuses" "^1.0.1" - "@bundled-es-modules/tough-cookie" "^0.1.6" - "@inquirer/confirm" "^5.0.0" - "@mswjs/interceptors" "^0.39.1" - "@open-draft/deferred-promise" "^2.2.0" - "@open-draft/until" "^2.1.0" - "@types/cookie" "^0.6.0" - "@types/statuses" "^2.0.4" - graphql "^16.8.1" - headers-polyfill "^4.0.2" + "@inquirer/confirm" "^6.0.11" + "@mswjs/interceptors" "^0.41.3" + "@open-draft/deferred-promise" "^3.0.0" + "@types/statuses" "^2.0.6" + cookie "^1.1.1" + graphql "^16.13.2" + headers-polyfill "^5.0.1" is-node-process "^1.2.0" outvariant "^1.4.3" path-to-regexp "^6.3.0" picocolors "^1.1.1" + rettime "^0.11.11" + statuses "^2.0.2" strict-event-emitter "^0.5.1" - type-fest "^4.26.1" + tough-cookie "^6.0.1" + type-fest "^5.5.0" + until-async "^3.0.2" yargs "^17.7.2" -mute-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" - integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== +mute-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1" + integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw== nanoid@^3.3.11, nanoid@^3.3.8: version "3.3.11" @@ -7586,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" @@ -7605,6 +7308,11 @@ pngjs@^5.0.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + pointer-tracker@^2.0.3: version "2.5.3" resolved "https://registry.yarnpkg.com/pointer-tracker/-/pointer-tracker-2.5.3.tgz#5ed01f5ff023c649b2d7b20b07d68c3ac40642a6" @@ -7663,7 +7371,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.5.6, postcss@^8.5.3, postcss@^8.5.6: +postcss@8.5.6, postcss@^8.5.6: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -7695,15 +7403,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -pretty-format@^27.0.2: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -7844,11 +7543,6 @@ raw-body@^3.0.0: iconv-lite "0.6.3" unpipe "1.0.0" -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -8009,6 +7703,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +rettime@^0.11.11: + version "0.11.11" + resolved "https://registry.yarnpkg.com/rettime/-/rettime-0.11.11.tgz#fe8fb192e1877bb0080fc1a640cb08eededd7d12" + integrity sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ== + reusify@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" @@ -8043,37 +7742,6 @@ rolldown@1.0.3: "@rolldown/binding-win32-arm64-msvc" "1.0.3" "@rolldown/binding-win32-x64-msvc" "1.0.3" -rollup@^4.34.9: - version "4.52.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.5.tgz#96982cdcaedcdd51b12359981f240f94304ec235" - integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw== - dependencies: - "@types/estree" "1.0.8" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.52.5" - "@rollup/rollup-android-arm64" "4.52.5" - "@rollup/rollup-darwin-arm64" "4.52.5" - "@rollup/rollup-darwin-x64" "4.52.5" - "@rollup/rollup-freebsd-arm64" "4.52.5" - "@rollup/rollup-freebsd-x64" "4.52.5" - "@rollup/rollup-linux-arm-gnueabihf" "4.52.5" - "@rollup/rollup-linux-arm-musleabihf" "4.52.5" - "@rollup/rollup-linux-arm64-gnu" "4.52.5" - "@rollup/rollup-linux-arm64-musl" "4.52.5" - "@rollup/rollup-linux-loong64-gnu" "4.52.5" - "@rollup/rollup-linux-ppc64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-musl" "4.52.5" - "@rollup/rollup-linux-s390x-gnu" "4.52.5" - "@rollup/rollup-linux-x64-gnu" "4.52.5" - "@rollup/rollup-linux-x64-musl" "4.52.5" - "@rollup/rollup-openharmony-arm64" "4.52.5" - "@rollup/rollup-win32-arm64-msvc" "4.52.5" - "@rollup/rollup-win32-ia32-msvc" "4.52.5" - "@rollup/rollup-win32-x64-gnu" "4.52.5" - "@rollup/rollup-win32-x64-msvc" "4.52.5" - fsevents "~2.3.2" - rou3@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/rou3/-/rou3-0.8.1.tgz#d18c9dae42bdd9cd4fffa77bc6731d5cfe92129a" @@ -8394,6 +8062,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz#e0b1d94c8660c68e6a24dc4e2b5c9e955ccf7e28" + integrity sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -8533,10 +8206,10 @@ sinon@20.0.0: diff "^7.0.0" supports-color "^7.2.0" -sirv@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.1.tgz#32a844794655b727f9e2867b777e0060fbe07bf3" - integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A== +sirv@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970" + integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g== dependencies: "@polka/url" "^1.0.0-next.24" mrmime "^2.0.0" @@ -8628,10 +8301,15 @@ statuses@2.0.1, statuses@^2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -std-env@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" - integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== +statuses@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +std-env@^4.0.0-rc.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f" + integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ== stop-iteration-iterator@^1.0.0, stop-iteration-iterator@^1.1.0: version "1.1.0" @@ -8934,6 +8612,11 @@ table@^6.9.0: string-width "^4.2.3" strip-ansi "^6.0.1" +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -8968,25 +8651,12 @@ tinybench@^2.9.0: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinyexec@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" - integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== - -tinyexec@^1.2.2: +tinyexec@^1.0.2, tinyexec@^1.2.2: version "1.2.4" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71" integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg== -tinyglobby@^0.2.13: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.3" - -tinyglobby@^0.2.16, tinyglobby@^0.2.17: +tinyglobby@^0.2.15, tinyglobby@^0.2.16, tinyglobby@^0.2.17: version "0.2.17" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== @@ -8994,20 +8664,22 @@ tinyglobby@^0.2.16, tinyglobby@^0.2.17: fdir "^6.5.0" picomatch "^4.0.4" -tinypool@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" - integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== +tinyrainbow@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421" + integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== -tinyrainbow@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" - integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== +tldts-core@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.4.3.tgz#d43401c0499cd884eeaf1ccf073df841a1e4e2dd" + integrity sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw== -tinyspy@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" - integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== +tldts@^7.0.5: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.4.3.tgz#536c93aecffc96d41ce5627a4b7e12f9c2cfceb5" + integrity sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg== + dependencies: + tldts-core "^7.4.3" tmp@^0.2.3: version "0.2.3" @@ -9041,6 +8713,13 @@ tough-cookie@^4.1.4: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76" + integrity sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw== + dependencies: + tldts "^7.0.5" + tr46@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.0.tgz#4a077922360ae807e172075ce5beb79b36e4a101" @@ -9092,20 +8771,17 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== -type-fest@^4.26.1: - version "4.39.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.0.tgz#c7758be50a83a5b879e7a59ea52421e9816b3928" - integrity sha512-w2IGJU1tIgcrepg9ZJ82d8UmItNQtOFJG0HCUE3SzMokKkTsruVDALl2fAdiEzJlfduoU+VyXJWIIUZ+6jV+nw== +type-fest@^5.5.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" + integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== + dependencies: + tagged-tag "^1.0.0" type-is@^2.0.0, type-is@^2.0.1: version "2.0.1" @@ -9261,6 +8937,11 @@ unstorage@^1.17.5: ofetch "^1.5.1" ufo "^1.6.3" +until-async@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/until-async/-/until-async-3.0.2.tgz#447f1531fdd7bb2b4c7a98869bdb1a4c2a23865f" + integrity sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw== + untildify@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" @@ -9332,17 +9013,6 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vite-node@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.3.tgz#d021ced40b5a057305eaea9ce62c610c33b60a48" - integrity sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA== - dependencies: - cac "^6.7.14" - debug "^4.4.0" - es-module-lexer "^1.7.0" - pathe "^2.0.3" - vite "^5.0.0 || ^6.0.0" - vite-plugin-eslint2@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/vite-plugin-eslint2/-/vite-plugin-eslint2-5.1.0.tgz#c796d4dc852b35f91db508946a4833589adea319" @@ -9359,21 +9029,7 @@ vite-plugin-stylelint@^6.1.0: "@rollup/pluginutils" "^5.3.0" debug "^4.4.3" -"vite@^5.0.0 || ^6.0.0": - version "6.4.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96" - integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g== - dependencies: - esbuild "^0.25.0" - fdir "^6.4.4" - picomatch "^4.0.2" - postcss "^8.5.3" - rollup "^4.34.9" - tinyglobby "^0.2.13" - optionalDependencies: - fsevents "~2.3.3" - -vite@^8.0.0: +"vite@^6.0.0 || ^7.0.0 || ^8.0.0", vite@^8.0.0: version "8.0.16" resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6" integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== @@ -9386,31 +9042,30 @@ vite@^8.0.0: optionalDependencies: fsevents "~2.3.3" -vitest@^3.0.7: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.1.3.tgz#0b0b01932408cd3af61867f4468d28bd83406ffb" - integrity sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw== +vitest@^4.1.7: + version "4.1.9" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.9.tgz#98f22fbd70e2a18c4a92bb20624bc92e5dfac5f3" + integrity sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ== dependencies: - "@vitest/expect" "3.1.3" - "@vitest/mocker" "3.1.3" - "@vitest/pretty-format" "^3.1.3" - "@vitest/runner" "3.1.3" - "@vitest/snapshot" "3.1.3" - "@vitest/spy" "3.1.3" - "@vitest/utils" "3.1.3" - chai "^5.2.0" - debug "^4.4.0" - expect-type "^1.2.1" - magic-string "^0.30.17" + "@vitest/expect" "4.1.9" + "@vitest/mocker" "4.1.9" + "@vitest/pretty-format" "4.1.9" + "@vitest/runner" "4.1.9" + "@vitest/snapshot" "4.1.9" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + es-module-lexer "^2.0.0" + expect-type "^1.3.0" + magic-string "^0.30.21" + obug "^2.1.1" pathe "^2.0.3" - std-env "^3.9.0" + picomatch "^4.0.3" + std-env "^4.0.0-rc.1" tinybench "^2.9.0" - tinyexec "^0.3.2" - tinyglobby "^0.2.13" - tinypool "^1.0.2" - tinyrainbow "^2.0.0" - vite "^5.0.0 || ^6.0.0" - vite-node "3.1.3" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" + vite "^6.0.0 || ^7.0.0 || ^8.0.0" why-is-node-running "^2.3.0" vue-component-type-helpers@^2.0.0: @@ -9699,12 +9354,7 @@ ws@^8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== -ws@^8.18.1: - version "8.18.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" - integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== - -ws@^8.21.0: +ws@^8.19.0, ws@^8.21.0: version "8.21.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== @@ -9843,11 +9493,6 @@ yocto-queue@^1.2.1: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== -yoctocolors-cjs@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" - integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== - zip-stream@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135"