Merge remote-tracking branch 'origin/develop' into more-fixes

This commit is contained in:
Henry Jameson 2026-06-26 15:29:08 +03:00
commit 6810d0fb9f
127 changed files with 4929 additions and 5175 deletions

View file

@ -73,7 +73,7 @@ test:
e2e-pleroma: e2e-pleroma:
stage: test stage: test
image: mcr.microsoft.com/playwright:v1.57.0-jammy image: mcr.microsoft.com/playwright:v1.61.0-jammy
services: services:
- name: postgres:15-alpine - name: postgres:15-alpine
alias: db alias: db
@ -175,7 +175,7 @@ e2e-pleroma:
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_TARGET: http://pleroma:4000
VITE_PROXY_ORIGIN: http://localhost:4000 VITE_PROXY_ORIGIN: http://localhost:4000
E2E_BASE_URL: http://localhost:8080 E2E_BASE_URL: http://localhost:8099
script: script:
- npm install -g yarn@1.22.22 - npm install -g yarn@1.22.22
- yarn --frozen-lockfile - yarn --frozen-lockfile

View file

@ -25,12 +25,12 @@ variables:
steps: steps:
test: test:
image: mcr.microsoft.com/playwright:v1.57.0-jammy image: mcr.microsoft.com/playwright:v1.61.0-jammy
entrypoint: *script_file_entrypoint entrypoint: *script_file_entrypoint
environment: environment:
APT_CACHE_DIR: apt-cache APT_CACHE_DIR: apt-cache
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
E2E_BASE_URL: http://localhost:8080 E2E_BASE_URL: http://localhost:8099
FF_NETWORK_PER_BUILD: "true" FF_NETWORK_PER_BUILD: "true"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
VITE_PROXY_ORIGIN: "http://pleroma:4000" VITE_PROXY_ORIGIN: "http://pleroma:4000"

View file

@ -25,7 +25,7 @@ variables:
steps: steps:
test: test:
image: mcr.microsoft.com/playwright:v1.57.0-jammy image: mcr.microsoft.com/playwright:v1.61.0-jammy
environment: environment:
APT_CACHE_DIR: apt-cache APT_CACHE_DIR: apt-cache
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive

View file

@ -46,6 +46,7 @@
"noUnusedLabels": "error", "noUnusedLabels": "error",
"noUnusedPrivateClassMembers": "error", "noUnusedPrivateClassMembers": "error",
"noUnusedVariables": "error", "noUnusedVariables": "error",
"noUnusedImports": "error",
"useIsNan": "error", "useIsNan": "error",
"useValidForDirection": "error", "useValidForDirection": "error",
"useValidTypeof": "error", "useValidTypeof": "error",

View file

@ -1,4 +1,3 @@
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { exactRegex } from '@rolldown/pluginutils' import { exactRegex } from '@rolldown/pluginutils'
@ -102,8 +101,12 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
}, },
}) })
const swBundle = await build(config) try {
return swBundle.output[0] const swBundle = await build(config)
return swBundle.output[0]
} catch (e) {
console.error('Error building ServiceWorker:', e)
}
}, },
}, },
closeBundle: { closeBundle: {
@ -112,7 +115,11 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
async handler() { async handler() {
if (process.env.VITEST) return if (process.env.VITEST) return
console.info('Building service worker for production') console.info('Building service worker for production')
await build(config) try {
await build(config)
} catch (e) {
console.error('Error building ServiceWorker:', e)
}
}, },
}, },
} }

View file

View file

@ -39,6 +39,8 @@ services:
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 60 retries: 60
ports:
- 4000:4000
e2e: e2e:
build: build:
@ -51,7 +53,7 @@ services:
CI: "1" CI: "1"
VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_TARGET: http://pleroma:4000
VITE_PROXY_ORIGIN: http://localhost: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_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
command: ["yarn", "e2e:pw"] command: ["yarn", "e2e:pw"]

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.57.0-jammy FROM mcr.microsoft.com/playwright:v1.61.0-jammy
WORKDIR /app WORKDIR /app

View file

@ -68,8 +68,9 @@
"@vitejs/devtools": "^0.3.1", "@vitejs/devtools": "^0.3.1",
"@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.5", "@vitejs/plugin-vue-jsx": "^5.1.5",
"@vitest/browser": "^3.0.7", "@vitest/browser-playwright": "^4.1.7",
"@vitest/ui": "^3.0.7", "@vitest/browser": "^4.1.7",
"@vitest/ui": "^4.1.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.5.0", "@vue/babel-plugin-jsx": "1.5.0",
"@vue/compiler-sfc": "3.5.22", "@vue/compiler-sfc": "3.5.22",
@ -95,10 +96,10 @@
"http-proxy-middleware": "3.0.5", "http-proxy-middleware": "3.0.5",
"iso-639-1": "3.1.5", "iso-639-1": "3.1.5",
"lodash": "4.17.21", "lodash": "4.17.21",
"msw": "2.10.5", "msw": "2.14.6",
"nightwatch": "3.12.2", "nightwatch": "3.12.2",
"oxc": "^1.0.1", "oxc": "^1.0.1",
"playwright": "1.57.0", "playwright": "1.61.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
@ -118,7 +119,7 @@
"vite": "^8.0.0", "vite": "^8.0.0",
"vite-plugin-eslint2": "^5.1.0", "vite-plugin-eslint2": "^5.1.0",
"vite-plugin-stylelint": "^6.1.0", "vite-plugin-stylelint": "^6.1.0",
"vitest": "^3.0.7", "vitest": "^4.1.7",
"vue-eslint-parser": "10.2.0" "vue-eslint-parser": "10.2.0"
}, },
"type": "module", "type": "module",

View file

@ -21,9 +21,6 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useShoutStore } from 'src/stores/shout.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 // Helper to unwrap reactive proxies
window.toValue = (x) => JSON.parse(JSON.stringify(x)) window.toValue = (x) => JSON.parse(JSON.stringify(x))

491
src/api/admin.js Normal file
View file

@ -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',
})

87
src/api/chats.js Normal file
View file

@ -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,
})

135
src/api/helpers.js Normal file
View file

@ -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 {}
}
}

45
src/api/mfa.js Normal file
View file

@ -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,
})
}

146
src/api/oauth.js Normal file
View file

@ -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,
})
}

279
src/api/public.js Normal file
View file

@ -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 }),
})

198
src/api/timelines.js Normal file
View file

@ -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,
}
})
}

924
src/api/user.js Normal file
View file

@ -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,
})
}

176
src/api/websocket.js Normal file
View file

@ -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,
})

View file

@ -19,7 +19,6 @@ import {
config.autoAddCss = false config.autoAddCss = false
import App from '../App.vue' 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 FaviconService from '../services/favicon_service/favicon_service.js'
import { applyStyleConfig } from '../services/style_setter/style_setter.js' import { applyStyleConfig } from '../services/style_setter/style_setter.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
@ -29,7 +28,6 @@ import {
} from '../services/window_utils/window_utils' } from '../services/window_utils/window_utils'
import routes from './routes' import routes from './routes'
import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useI18nStore } from 'src/stores/i18n' 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 { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_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 { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.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 resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map((uri) => uri.split('/').pop()) const nicknames = accounts.map((uri) => uri.split('/').pop())
useInstanceStore().set({ useInstanceStore().set({
@ -461,14 +449,13 @@ const setConfig = async ({ store }) => {
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
getAppSecret({ store })
await setSettings({ store, apiConfig, staticConfig }) await setSettings({ store, apiConfig, staticConfig })
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
const oauth = useOAuthStore() const oauth = useOAuthStore()
if (oauth.getUserToken) { if (oauth.userToken) {
return store.dispatch('loginUser', oauth.getUserToken) return store.dispatch('loginUser', oauth.userToken)
} }
return Promise.resolve() return Promise.resolve()
} }
@ -578,10 +565,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
getInstanceConfig({ store }), getInstanceConfig({ store }),
]).catch((e) => Promise.reject(e)) ]).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 }) getTOS({ store })
getStickers({ store }) getStickers({ store })

View file

@ -35,9 +35,7 @@ const AnnouncementsPage = {
canPostAnnouncement() { canPostAnnouncement() {
return ( return (
this.currentUser && this.currentUser &&
this.currentUser.privileges.has( this.currentUser.privileges.has('announcements_manage_announcements')
'announcements_manage_announcements',
)
) )
}, },
}, },

View file

@ -2,7 +2,6 @@ import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.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 nsfwImage from '../../assets/nsfw.png'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'

View file

@ -1,8 +1,10 @@
import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue' 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 { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { fetchBookmarkFolders } from 'src/api/user.js'
const BookmarkFolderEdit = { const BookmarkFolderEdit = {
data() { data() {
@ -22,8 +24,10 @@ const BookmarkFolderEdit = {
}, },
created() { created() {
if (!this.id) return 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) const folder = folders.find((folder) => folder.id === this.id)
if (!folder) return if (!folder) return

View file

@ -5,7 +5,6 @@ import { mapGetters, mapState } from 'vuex'
import ChatMessage from 'src/components/chat_message/chat_message.vue' import ChatMessage from 'src/components/chat_message/chat_message.vue'
import ChatTitle from 'src/components/chat_title/chat_title.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.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 chatService from '../../services/chat_service/chat_service.js'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
@ -17,6 +16,14 @@ import {
} from './chat_layout_utils.js' } from './chat_layout_utils.js'
import { useInterfaceStore } from 'src/stores/interface.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 { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
@ -115,7 +122,6 @@ const Chat = {
mobileLayout: (store) => store.layoutType === 'mobile', mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
...mapState({ ...mapState({
backendInteractor: (state) => state.api.backendInteractor,
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
currentUser: (state) => state.users.currentUser, currentUser: (state) => state.users.currentUser,
}), }),
@ -267,43 +273,48 @@ const Chat = {
const fetchOlderMessages = !!maxId const fetchOlderMessages = !!maxId
const sinceId = fetchLatest && chatMessageService.maxId const sinceId = fetchLatest && chatMessageService.maxId
return this.backendInteractor return chatMessages({
.chatMessages({ id: chatId, maxId, sinceId }) id: chatId,
.then((messages) => { maxId,
// Clear the current chat in case we're recovering from a ws connection loss. sinceId,
if (isFirstFetch) { credentials: useOAuthStore().token,
chatService.clear(chatMessageService) }).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() const positionBeforeUpdate = getScrollPosition()
this.$store this.$store
.dispatch('addChatMessages', { chatId, messages }) .dispatch('addChatMessages', { chatId, messages })
.then(() => { .then(() => {
this.$nextTick(() => { this.$nextTick(() => {
if (fetchOlderMessages) { if (fetchOlderMessages) {
this.handleScrollUp(positionBeforeUpdate) this.handleScrollUp(positionBeforeUpdate)
} }
// In vertical screens, the first batch of fetched messages may not always take the // In vertical screens, the first batch of fetched messages may not always take the
// full height of the scrollable container. // full height of the scrollable container.
// If this is the case, we want to fetch the messages until 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. // is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable() && messages.length > 0) { if (!isScrollable() && messages.length > 0) {
this.fetchChat({ this.fetchChat({
maxId: this.currentChatMessageService.minId, maxId: this.currentChatMessageService.minId,
}) })
} }
})
}) })
}) })
})
}, },
async startFetching() { async startFetching() {
let chat = this.findOpenedChatByRecipientId(this.recipientId) let chat = this.findOpenedChatByRecipientId(this.recipientId)
if (!chat) { if (!chat) {
try { try {
chat = await this.backendInteractor.getOrCreateChat({ const { data } = await getOrCreateChat({
accountId: this.recipientId, accountId: this.recipientId,
credentials: useOAuthStore().token,
}) })
chat = data
} catch (e) { } catch (e) {
console.error('Error creating or getting a chat', e) console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true this.errorLoadingChat = true
@ -369,9 +380,11 @@ const Chat = {
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
if (retriesLeft <= 0) return if (retriesLeft <= 0) return
this.backendInteractor sendChatMessage({
.sendChatMessage(params) params,
.then((data) => { credentials: useOAuthStore().token,
})
.then(({ data }) => {
this.$store.dispatch('addChatMessages', { this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id, chatId: this.currentChat.id,
updateMaxId: false, updateMaxId: false,

View file

@ -1,6 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import Attachment from 'src/components/attachment/attachment.vue' import Attachment from 'src/components/attachment/attachment.vue'
import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue' import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'

View file

@ -3,6 +3,8 @@ import { mapGetters, mapState } from 'vuex'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.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 { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
@ -22,7 +24,9 @@ const chatNew = {
} }
}, },
async created() { async created() {
const { chats } = await this.backendInteractor.chats() const { chats } = await chats({
credentials: useOAuthStore().token,
})
chats.forEach((chat) => this.suggestions.push(chat.account)) chats.forEach((chat) => this.suggestions.push(chat.account))
}, },
computed: { computed: {
@ -38,7 +42,6 @@ const chatNew = {
}, },
...mapState({ ...mapState({
currentUser: (state) => state.users.currentUser, currentUser: (state) => state.users.currentUser,
backendInteractor: (state) => state.api.backendInteractor,
}), }),
...mapGetters(['findUser']), ...mapGetters(['findUser']),
}, },

View file

@ -1,5 +1,3 @@
import { defineAsyncComponent } from 'vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import UserPopover from 'src/components/user_popover/user_popover.vue' import UserPopover from 'src/components/user_popover/user_popover.vue'

View file

@ -5,10 +5,13 @@ import { mapState } from 'vuex'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue' 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 QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
import ThreadTree from 'src/components/thread_tree/thread_tree.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 { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -436,22 +439,26 @@ const conversation = {
methods: { methods: {
fetchConversation() { fetchConversation() {
if (this.status) { if (this.status) {
this.$store.state.api.backendInteractor fetchConversation({
.fetchConversation({ id: this.statusId }) id: this.statusId,
.then(({ ancestors, descendants }) => { credentials: useOAuthStore().token,
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) }).then(({ data: { ancestors, descendants } }) => {
this.$store.dispatch('addNewStatuses', { statuses: descendants }) this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.setHighlight(this.originalStatusId) this.$store.dispatch('addNewStatuses', { statuses: descendants })
}) this.setHighlight(this.originalStatusId)
})
} else { } else {
this.loadStatusError = null this.loadStatusError = null
this.$store.state.api.backendInteractor fetchStatus({
.fetchStatus({ id: this.statusId }) id: this.statusId,
.then((status) => { credentials: useOAuthStore().token,
})
.then(({ data: status }) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation() this.fetchConversation()
}) })
.catch((error) => { .catch((error) => {
console.error(error)
this.loadStatusError = error this.loadStatusError = error
}) })
} }

View file

@ -1,5 +1,3 @@
import { useEmojiStore } from 'src/stores/emoji.js'
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions: * data: object providing source information for specific types of suggestions:

View file

@ -6,7 +6,6 @@ import Popover from 'src/components/popover/popover.vue'
import { ensureFinalFallback } from '../../i18n/languages.js' import { ensureFinalFallback } from '../../i18n/languages.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'

View file

@ -4,6 +4,9 @@ import { notificationsFromStore } from '../../services/notification_utils/notifi
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' 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 = { const FollowRequestCard = {
props: ['user'], props: ['user'],
@ -48,7 +51,10 @@ const FollowRequestCard = {
} }
}, },
doApprove() { 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('removeFollowRequest', this.user)
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
@ -70,12 +76,14 @@ const FollowRequestCard = {
}, },
doDeny() { doDeny() {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id }) denyUser({
.then(() => { id: this.user.id,
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) credentials: useOAuthStore().token,
this.$store.dispatch('removeFollowRequest', this.user) }).then(() => {
}) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
}, },
}, },

View file

@ -4,7 +4,6 @@ import { mapState } from 'vuex'
import { getListEntries } from 'src/components/navigation/filter.js' import { getListEntries } from 'src/components/navigation/filter.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useListsStore } from 'src/stores/lists.js' import { useListsStore } from 'src/stores/lists.js'
export const ListsMenuContent = { export const ListsMenuContent = {

View file

@ -1,12 +1,12 @@
import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia' import { mapActions, mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import oauthApi from '../../services/new_api/oauth.js'
import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useAuthFlowStore } from 'src/stores/auth_flow.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faTimes } from '@fortawesome/free-solid-svg-icons' import { faTimes } from '@fortawesome/free-solid-svg-icons'
@ -35,19 +35,13 @@ const LoginForm = {
this.isTokenAuth ? this.submitToken() : this.submitPassword() this.isTokenAuth ? this.submitToken() : this.submitPassword()
}, },
submitToken() { submitToken() {
const data = {
instance: this.server,
commit: this.$store.commit,
}
// NOTE: we do not really need the app token, but obtaining a token and // 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. // calling verify_credentials is the only way to ensure the app still works.
this.ensureAppToken().then(() => { this.ensureAppToken().then(() => {
const app = { window.location.href = getLoginUrl({
clientId: this.clientId, clientId: this.clientId,
clientSecret: this.clientSecret, instance: this.server,
} })
oauthApi.login({ ...app, ...data })
}) })
}, },
submitPassword() { submitPassword() {
@ -56,37 +50,32 @@ const LoginForm = {
// NOTE: we do not really need the app token, but obtaining a token and // 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. // calling verify_credentials is the only way to ensure the app still works.
this.ensureAppToken().then(() => { this.ensureAppToken().then(() => {
const app = { getTokenWithCredentials({
clientId: this.clientId, clientId: this.clientId,
clientSecret: this.clientSecret, clientSecret: this.clientSecret,
} instance: this.server,
username: this.user.username,
oauthApi password: this.user.password,
.getTokenWithCredentials({ })
...app, .then(({ data: result }) => {
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
}
this.login(result).then(() => { this.login(result).then(() => {
this.$router.push({ name: 'friends' }) 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() { clearError() {

View file

@ -1,7 +1,6 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Modal from 'src/components/modal/modal.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 GestureService from '../../services/gesture_service/gesture_service'
import { useMediaViewerStore } from 'src/stores/media_viewer.js' import { useMediaViewerStore } from 'src/stores/media_viewer.js'

View file

@ -1,6 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'

View file

@ -1,11 +1,11 @@
import { mapActions, mapState, mapStores } from 'pinia' import { mapActions, mapState, mapStores } from 'pinia'
import mfaApi from '../../services/new_api/mfa.js'
import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useAuthFlowStore } from 'src/stores/auth_flow.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { verifyRecoveryCode } from 'src/api/mfa.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faTimes } from '@fortawesome/free-solid-svg-icons' import { faTimes } from '@fortawesome/free-solid-svg-icons'
@ -43,18 +43,18 @@ export default {
code: this.code, code: this.code,
} }
mfaApi.verifyRecoveryCode(data).then((result) => { verifyRecoveryCode(data)
if (result.error) { .then((result) => {
this.error = result.error this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
.catch((error) => {
this.error = error
this.code = null this.code = null
this.focusOnCodeInput() this.focusOnCodeInput()
return return
}
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
}) })
})
}, },
}, },
} }

View file

@ -1,11 +1,11 @@
import { mapActions, mapState, mapStores } from 'pinia' import { mapActions, mapState, mapStores } from 'pinia'
import mfaApi from '../../services/new_api/mfa.js'
import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useAuthFlowStore } from 'src/stores/auth_flow.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { verifyOTPCode } from 'src/api/mfa.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faTimes } from '@fortawesome/free-solid-svg-icons' import { faTimes } from '@fortawesome/free-solid-svg-icons'
@ -46,18 +46,18 @@ export default {
code: this.code, code: this.code,
} }
mfaApi.verifyOTPCode(data).then((result) => { verifyOTPCode(data)
if (result.error) { .then(({ data: result }) => {
this.error = result.error this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
.catch((error) => {
this.error = error
this.code = null this.code = null
this.focusOnCodeInput() this.focusOnCodeInput()
return return
}
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
}) })
})
}, },
}, },
} }

View file

@ -4,7 +4,6 @@ import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'

View file

@ -15,8 +15,10 @@ import {
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.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 { 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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -142,7 +144,10 @@ const Notification = {
} }
}, },
doApprove() { 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('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { this.$store.dispatch('markSingleNotificationAsSeen', {
id: this.notification.id, id: this.notification.id,
@ -163,14 +168,15 @@ const Notification = {
} }
}, },
doDeny() { doDeny() {
this.$store.state.api.backendInteractor denyUser({
.denyUser({ id: this.user.id }) id: this.user.id,
.then(() => { credentials: useOAuthStore().token,
this.$store.dispatch('dismissNotificationLocal', { }).then(() => {
id: this.notification.id, this.$store.dispatch('dismissNotificationLocal', {
}) id: this.notification.id,
this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
}, },
}, },

View file

@ -1,8 +1,8 @@
import oauth from '../../services/new_api/oauth.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { getToken } from 'src/api/oauth.js'
const oac = { const oac = {
props: ['code'], props: ['code'],
mounted() { mounted() {
@ -10,18 +10,16 @@ const oac = {
const oauthStore = useOAuthStore() const oauthStore = useOAuthStore()
const { clientId, clientSecret } = oauthStore const { clientId, clientSecret } = oauthStore
oauth getToken({
.getToken({ clientId,
clientId, clientSecret,
clientSecret, instance: useInstanceStore().server,
instance: useInstanceStore().server, code: this.code,
code: this.code, }).then(({ data: result }) => {
}) oauthStore.setToken(result.access_token)
.then((result) => { this.$store.dispatch('loginUser', result.access_token)
oauthStore.setToken(result.access_token) this.$router.push({ name: 'friends' })
this.$store.dispatch('loginUser', result.access_token) })
this.$router.push({ name: 'friends' })
})
} }
}, },
} }

View file

@ -1,10 +1,10 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import passwordResetApi from '../../services/new_api/password_reset.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { resetPassword } from 'src/api/public.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faTimes } from '@fortawesome/free-solid-svg-icons' import { faTimes } from '@fortawesome/free-solid-svg-icons'
@ -24,7 +24,7 @@ const passwordReset = {
...mapState({ ...mapState({
signedIn: (state) => !!state.users.currentUser, signedIn: (state) => !!state.users.currentUser,
}), }),
...mapPiniaState(useInstanceStore, ['server', 'mailerEnabled']), ...mapPiniaState(useInstanceStore, ['mailerEnabled']),
}, },
created() { created() {
if (this.signedIn) { if (this.signedIn) {
@ -44,9 +44,8 @@ const passwordReset = {
submit() { submit() {
this.isPending = true this.isPending = true
const email = this.user.email const email = this.user.email
const server = this.server
passwordResetApi({ server, email }) resetPassword({ email })
.then(({ status }) => { .then(({ status }) => {
this.isPending = false this.isPending = false
this.user.email = '' this.user.email = ''

View file

@ -1,7 +1,6 @@
import { debounce, map, reject, uniqBy } from 'lodash' import { debounce, map, reject, uniqBy } from 'lodash'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { mapGetters } from 'vuex'
import Attachment from 'src/components/attachment/attachment.vue' import Attachment from 'src/components/attachment/attachment.vue'
import Checkbox from 'src/components/checkbox/checkbox.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 // Don't apply preview if not loading, because it means
// user has closed the preview manually. // user has closed the preview manually.
if (!this.previewLoading) return if (!this.previewLoading) return
if (!data.error) { this.preview = data
this.preview = data
} else {
this.preview = { error: data.error }
}
}) })
.catch((error) => { .catch((error) => {
this.preview = { error } this.preview = { error }

View file

@ -1,5 +1,4 @@
import { get } from 'lodash' import { get } from 'lodash'
import { defineAsyncComponent } from 'vue'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'

View file

@ -1,5 +1,3 @@
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'

View file

@ -1,3 +1,7 @@
import { useOAuthStore } from 'src/stores/oauth.js'
import { fetchUser } from 'src/api/public.js'
const RemoteUserResolver = { const RemoteUserResolver = {
data: () => ({ data: () => ({
error: false, error: false,
@ -7,11 +11,12 @@ const RemoteUserResolver = {
}, },
methods: { methods: {
redirect() { redirect() {
const acct = const id = this.$route.params.username + '@' + this.$route.params.hostname
this.$route.params.username + '@' + this.$route.params.hostname fetchUser({
this.$store.state.api.backendInteractor id,
.fetchUser({ id: acct }) credentials: useOAuthStore().token,
.then((externalUser) => { })
.then(({ data: externalUser }) => {
if (externalUser.error) { if (externalUser.error) {
this.error = true this.error = true
} else { } else {

View file

@ -1,5 +1,3 @@
import { defineAsyncComponent } from 'vue'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue' import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'

View file

@ -2,7 +2,7 @@ import Checkbox from 'components/checkbox/checkbox.vue'
import Popover from 'components/popover/popover.vue' import Popover from 'components/popover/popover.vue'
import Select from 'components/select/select.vue' import Select from 'components/select/select.vue'
import StillImage from 'components/still-image/still-image.vue' import StillImage from 'components/still-image/still-image.vue'
import { assign, clone } from 'lodash' import { clone } from 'lodash'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' 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 SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue' import StringSetting from '../helpers/string_setting.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
@ -98,10 +99,10 @@ const EmojiTab = {
methods: { methods: {
reloadEmoji() { reloadEmoji() {
this.$store.state.api.backendInteractor.reloadEmoji() useAdminSettingsStore().reloadEmoji()
}, },
importFromFS() { importFromFS() {
this.$store.state.api.backendInteractor.importEmojiFromFS() useAdminSettingsStore().importEmojiFromFS()
}, },
emojiAddr(name) { emojiAddr(name) {
if (this.pack.remote !== undefined) { if (this.pack.remote !== undefined) {
@ -113,7 +114,7 @@ const EmojiTab = {
}, },
createEmojiPack() { createEmojiPack() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.createEmojiPack({ name: this.newPackName }) .createEmojiPack({ name: this.newPackName })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((resp) => { .then((resp) => {
@ -130,7 +131,7 @@ const EmojiTab = {
}) })
}, },
deleteEmojiPack() { deleteEmojiPack() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.deleteEmojiPack({ name: this.packName }) .deleteEmojiPack({ name: this.packName })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((resp) => { .then((resp) => {
@ -157,7 +158,7 @@ const EmojiTab = {
return edited !== def return edited !== def
}, },
savePackMetadata() { savePackMetadata() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }) .saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((resp) => { .then((resp) => {
@ -182,7 +183,7 @@ const EmojiTab = {
useEmojiStore() useEmojiStore()
.getAdminPacks( .getAdminPacks(
this.remotePackInstance, this.remotePackInstance,
this.$store.state.api.backendInteractor.listEmojiPacks, useAdminSettingsStore().listEmojiPacks,
) )
.then((allPacks) => { .then((allPacks) => {
this.knownLocalPacks = allPacks this.knownLocalPacks = allPacks
@ -195,7 +196,7 @@ const EmojiTab = {
useEmojiStore() useEmojiStore()
.getAdminPacks( .getAdminPacks(
this.remotePackInstance, this.remotePackInstance,
this.$store.state.api.backendInteractor.listRemoteEmojiPacks, useAdminSettingsStore().listRemoteEmojiPacks,
) )
.then((allPacks) => { .then((allPacks) => {
let inst = this.remotePackInstance let inst = this.remotePackInstance
@ -226,7 +227,7 @@ const EmojiTab = {
this.remotePackDownloadAs = this.pack.remote.baseName this.remotePackDownloadAs = this.pack.remote.baseName
} }
this.$store.state.api.backendInteractor useAdminSettingsStore()
.downloadRemoteEmojiPack({ .downloadRemoteEmojiPack({
instance: this.pack.remote.instance, instance: this.pack.remote.instance,
packName: this.pack.remote.baseName, packName: this.pack.remote.baseName,
@ -247,7 +248,7 @@ const EmojiTab = {
}) })
}, },
downloadRemoteURLPack() { downloadRemoteURLPack() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.downloadRemoteEmojiPackZIP({ .downloadRemoteEmojiPackZIP({
url: this.remotePackURL, url: this.remotePackURL,
packName: this.newPackName, packName: this.newPackName,
@ -268,7 +269,7 @@ const EmojiTab = {
}) })
}, },
downloadRemoteFilePack() { downloadRemoteFilePack() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.downloadRemoteEmojiPackZIP({ .downloadRemoteEmojiPackZIP({
file: this.remotePackFile[0], file: this.remotePackFile[0],
packName: this.newPackName, packName: this.newPackName,

View file

@ -71,7 +71,7 @@ const FrontendsTab = {
const payload = { name, ref } const payload = { name, ref }
this.working = true this.working = true
this.$store.state.api.backendInteractor useAdminSettingsStore()
.installFrontend({ payload }) .installFrontend({ payload })
.finally(() => { .finally(() => {
this.working = false this.working = false

View file

@ -1,5 +1,3 @@
import { isEmpty } from 'lodash'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'

View file

@ -153,6 +153,14 @@ import Popover from 'components/popover/popover.vue'
import SelectComponent from 'components/select/select.vue' import SelectComponent from 'components/select/select.vue'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { useOAuthStore } from 'src/stores/oauth.js'
import {
addNewEmojiFile,
deleteEmojiFile,
updateEmojiFile,
} from 'src/api/admin.js'
export default { export default {
components: { components: {
Popover, Popover,
@ -243,14 +251,14 @@ export default {
saveEditedEmoji() { saveEditedEmoji() {
if (!this.isEdited) return if (!this.isEdited) return
this.$store.state.api.backendInteractor updateEmojiFile({
.updateEmojiFile({ packName: this.packName,
packName: this.packName, shortcode: this.shortcode,
shortcode: this.shortcode, newShortcode: this.editedShortcode,
newShortcode: this.editedShortcode, newFilename: this.editedFile,
newFilename: this.editedFile, force: false,
force: false, credentials: useOAuthStore().token,
}) })
.then((resp) => { .then((resp) => {
if (resp.error !== undefined) { if (resp.error !== undefined) {
this.$emit('displayError', resp.error) this.$emit('displayError', resp.error)
@ -263,18 +271,18 @@ export default {
}, },
uploadEmoji() { uploadEmoji() {
let packName = this.remote === undefined ? this.packName : this.copyToPack let packName = this.remote === undefined ? this.packName : this.copyToPack
this.$store.state.api.backendInteractor addNewEmojiFile({
.addNewEmojiFile({ packName: packName,
packName: packName, file:
file: this.remote === undefined
this.remote === undefined ? this.uploadURL !== ''
? this.uploadURL !== '' ? this.uploadURL
? this.uploadURL : this.uploadFile[0]
: this.uploadFile[0] : this.emojiAddr(this.file),
: this.emojiAddr(this.file), shortcode: this.editedShortcode,
shortcode: this.editedShortcode, filename: this.editedFile,
filename: this.editedFile, credentials: useOAuthStore().token,
}) })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((resp) => { .then((resp) => {
if (resp.error !== undefined) { if (resp.error !== undefined) {
@ -297,8 +305,11 @@ export default {
deleteEmoji() { deleteEmoji() {
this.deleteModalVisible = false this.deleteModalVisible = false
this.$store.state.api.backendInteractor deleteEmojiFile({
.deleteEmojiFile({ packName: this.packName, shortcode: this.shortcode }) packName: this.packName,
shortcode: this.shortcode,
credentials: useOAuthStore().token,
})
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((resp) => { .then((resp) => {
if (resp.error !== undefined) { if (resp.error !== undefined) {

View file

@ -5,7 +5,6 @@ import LocalSettingIndicator from './local_setting_indicator.vue'
import ModifiedIndicator from './modified_indicator.vue' import ModifiedIndicator from './modified_indicator.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'

View file

@ -1,8 +1,7 @@
// eslint-disable-next-line no-unused // eslint-disable-next-line no-unused
import { throttle } from 'lodash' import { mapState as mapPiniaState } from 'pinia'
import { mapState as mapPiniaState, mapState } from 'pinia' import { Fragment } from 'vue'
import { Fragment, h } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'

View file

@ -1,4 +1,3 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import AuthTab from './admin_tabs/auth_tab.vue' import AuthTab from './admin_tabs/auth_tab.vue'
import EmojiTab from './admin_tabs/emoji_tab.vue' import EmojiTab from './admin_tabs/emoji_tab.vue'
import FederationTab from './admin_tabs/federation_tab.vue' import FederationTab from './admin_tabs/federation_tab.vue'

View file

@ -12,7 +12,9 @@ import Preview from './old_theme_tab/theme_preview.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { normalizeThemeData, useInterfaceStore } from 'src/stores/interface.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 { newImporter } from 'src/services/export_import/export_import.js'
import { import {
adoptStyleSheets, adoptStyleSheets,
@ -484,9 +486,11 @@ const AppearanceTab = {
} }
this.backgroundUploading = true this.backgroundUploading = true
this.$store.state.api.backendInteractor updateProfileImages({
.updateProfileImages({ background }) background,
.then((data) => { credentials: useOAuthStore().token,
})
.then(({ data }) => {
this.$store.commit('addNewUsers', [data]) this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data) this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null this.backgroundPreview = null

View file

@ -14,8 +14,10 @@ import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_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 { useSyncConfigStore } from 'src/stores/sync_config.js'
import { updateProfile } from 'src/api/user.js'
import localeService from 'src/services/locale/locale.service.js' import localeService from 'src/services/locale/locale.service.js'
import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js' import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js'
@ -164,12 +166,13 @@ const ComposingTab = {
), ),
} }
this.$store.state.api.backendInteractor updateProfile({
.updateProfile({ params }) params,
.then((user) => { credentials: useOAuthStore().token,
this.$store.commit('addNewUsers', [user]) }).then(({ data: user }) => {
this.$store.commit('setCurrentUser', user) this.$store.commit('addNewUsers', [user])
}) this.$store.commit('setCurrentUser', user)
})
}, },
updateFont(key, value) { updateFont(key, value) {
useSyncConfigStore().setSimplePrefAndSave({ useSyncConfigStore().setSimplePrefAndSave({

View file

@ -4,8 +4,20 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import Exporter from 'src/components/exporter/exporter.vue' import Exporter from 'src/components/exporter/exporter.vue'
import Importer from 'src/components/importer/importer.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 { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
import {
addBackup,
exportFriends,
fetchBlocks,
fetchMutes,
importBlocks,
importFollows,
importMutes,
listBackups,
} from 'src/api/user.js'
const DataImportExportTab = { const DataImportExportTab = {
data() { data() {
return { return {
@ -28,48 +40,57 @@ const DataImportExportTab = {
}, },
computed: { computed: {
...mapState({ ...mapState({
backendInteractor: (state) => state.api.backendInteractor,
user: (state) => state.users.currentUser, user: (state) => state.users.currentUser,
}), }),
}, },
methods: { methods: {
getFollowsContent() { getFollowsContent() {
return this.backendInteractor return exportFriends({
.exportFriends({ id: this.user.id }) id: this.user.id,
.then(this.generateExportableUsersContent) credentials: useOAuthStore().token,
}).then(this.generateExportableUsersContent)
}, },
getBlocksContent() { getBlocksContent() {
return this.backendInteractor return fetchBlocks({
.fetchBlocks() credentials: useOAuthStore().token,
.then(this.generateExportableUsersContent) }).then(this.generateExportableUsersContent)
}, },
getMutesContent() { getMutesContent() {
return this.backendInteractor return fetchMutes({
.fetchMutes() credentials: useOAuthStore().token,
.then(this.generateExportableUsersContent) }).then(this.generateExportableUsersContent)
}, },
importFollows(file) { importFollows(file) {
return this.backendInteractor.importFollows({ file }).then((status) => { return importFollows({
file,
credentials: useOAuthStore().token,
}).then(({ data: status }) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
} }
}) })
}, },
importBlocks(file) { importBlocks(file) {
return this.backendInteractor.importBlocks({ file }).then((status) => { return importBlocks({
file,
credentials: useOAuthStore().token,
}).then(({ data: status }) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
} }
}) })
}, },
importMutes(file) { importMutes(file) {
return this.backendInteractor.importMutes({ file }).then((status) => { return importMutes({
file,
credentials: useOAuthStore().token,
}).then(({ data: status }) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
} }
}) })
}, },
generateExportableUsersContent(users) { generateExportableUsersContent({ data: users }) {
// Get addresses // Get addresses
return users return users
.map((user) => { .map((user) => {
@ -83,8 +104,9 @@ const DataImportExportTab = {
.join('\n') .join('\n')
}, },
addBackup() { addBackup() {
this.$store.state.api.backendInteractor addBackup({
.addBackup() credentials: useOAuthStore().token,
})
.then(() => { .then(() => {
this.addedBackup = true this.addedBackup = true
this.addBackupError = false this.addBackupError = false
@ -96,9 +118,10 @@ const DataImportExportTab = {
.then(() => this.fetchBackups()) .then(() => this.fetchBackups())
}, },
fetchBackups() { fetchBackups() {
this.$store.state.api.backendInteractor listBackups({
.listBackups() credentials: useOAuthStore().token,
.then((res) => { })
.then(({ data: res }) => {
this.backups = res this.backups = res
this.listBackupsError = false this.listBackupsError = false
}) })

View file

@ -8,12 +8,13 @@ import FloatSetting from '../helpers/float_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue' import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_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 { useSyncConfigStore } from 'src/stores/sync_config.js'
import { updateProfile } from 'src/api/user.js'
import localeService from 'src/services/locale/locale.service.js' import localeService from 'src/services/locale/locale.service.js'
const GeneralTab = { const GeneralTab = {
@ -58,12 +59,13 @@ const GeneralTab = {
), ),
} }
this.$store.state.api.backendInteractor updateProfile({
.updateProfile({ params }) params,
.then((user) => { credentials: useOAuthStore().token,
this.$store.commit('addNewUsers', [user]) }).then(({ data: user }) => {
this.$store.commit('setCurrentUser', user) this.$store.commit('addNewUsers', [user])
}) this.$store.commit('setCurrentUser', user)
})
}, },
updateFont(path, value) { updateFont(path, value) {
useLocalConfigStore().set({ path, value }) useLocalConfigStore().set({ path, value })

View file

@ -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 Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import BlockCard from 'src/components/block_card/block_card.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 TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js' import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
import { importBlocks, importFollows } from 'src/api/user.js'
const MutesAndBlocks = { const MutesAndBlocks = {
data() { data() {
return { return {
@ -54,22 +57,24 @@ const MutesAndBlocks = {
return () => this.$store.dispatch('fetch' + group, this.userId) return () => this.$store.dispatch('fetch' + group, this.userId)
}, },
importFollows(file) { importFollows(file) {
return this.$store.state.api.backendInteractor return importFollows({
.importFollows({ file }) file,
.then((status) => { credentials: useOAuthStore().token,
if (!status) { }).then(({ data: status }) => {
throw new Error('failed') if (!status) {
} throw new Error('failed')
}) }
})
}, },
importBlocks(file) { importBlocks(file) {
return this.$store.state.api.backendInteractor return importBlocks({
.importBlocks({ file }) file,
.then((status) => { credentials: useOAuthStore().token,
if (!status) { }).then(({ data: status }) => {
throw new Error('failed') if (!status) {
} throw new Error('failed')
}) }
})
}, },
generateExportableUsersContent(users) { generateExportableUsersContent(users) {
// Get addresses // Get addresses

View file

@ -1,6 +1,10 @@
import BooleanSetting from '../helpers/boolean_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { updateNotificationSettings } from 'src/api/user.js'
const NotificationsTab = { const NotificationsTab = {
data() { data() {
return { return {
@ -27,7 +31,8 @@ const NotificationsTab = {
}, },
methods: { methods: {
updateNotificationSettings() { updateNotificationSettings() {
this.$store.state.api.backendInteractor.updateNotificationSettings({ updateNotificationSettings({
credentials: useOAuthStore().token,
settings: this.notificationSettings, settings: this.notificationSettings,
}) })
}, },

View file

@ -6,7 +6,6 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue' import UnitSetting from '../helpers/unit_setting.vue'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
const PostsTab = { const PostsTab = {
data() { data() {

View file

@ -3,6 +3,10 @@ import UserCard from 'src/components/user_card/user_card.vue'
import BooleanSetting from '../helpers/boolean_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faCircleNotch, faCircleNotch,
@ -35,10 +39,11 @@ const ProfileTab = {
const params = { const params = {
locked: this.locked, locked: this.locked,
} }
updateProfile({
this.$store.state.api.backendInteractor params,
.updateProfile({ params }) credentials: useOAuthStore().token,
.then((user) => { })
.then(({ data: user }) => {
this.$store.commit('addNewUsers', [user]) this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user) this.$store.commit('setCurrentUser', user)
}) })

View file

@ -1,10 +1,18 @@
import VueQrcode from '@chenfengyuan/vue-qrcode' import VueQrcode from '@chenfengyuan/vue-qrcode'
import { mapState } from 'vuex'
import Confirm from './confirm.vue' import Confirm from './confirm.vue'
import RecoveryCodes from './mfa_backup_codes.vue' import RecoveryCodes from './mfa_backup_codes.vue'
import TOTP from './mfa_totp.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 = { const Mfa = {
data: () => ({ data: () => ({
settings: { settings: {
@ -71,9 +79,6 @@ const Mfa = {
confirmNewBackupCodes() { confirmNewBackupCodes() {
return this.backupCodes.getNewCodes return this.backupCodes.getNewCodes
}, },
...mapState({
backendInteractor: (state) => state.api.backendInteractor,
}),
}, },
methods: { methods: {
@ -87,7 +92,9 @@ const Mfa = {
this.backupCodes.inProgress = true this.backupCodes.inProgress = true
this.backupCodes.codes = [] this.backupCodes.codes = []
return this.backendInteractor.generateMfaBackupCodes().then((res) => { return generateMfaBackupCodes({
credentials: useOAuthStore().token,
}).then(({ data: res }) => {
this.backupCodes.codes = res.codes this.backupCodes.codes = res.codes
this.backupCodes.inProgress = false this.backupCodes.inProgress = false
}) })
@ -112,7 +119,9 @@ const Mfa = {
// prepare setup OTP // prepare setup OTP
this.setupState.state = 'setupOTP' this.setupState.state = 'setupOTP'
this.setupState.setupOTPState = 'prepare' this.setupState.setupOTPState = 'prepare'
this.backendInteractor.mfaSetupOTP().then((res) => { mfaSetupOTP({
credentials: useOAuthStore().token,
}).then(({ data: res }) => {
this.otpSettings = res this.otpSettings = res
this.setupState.setupOTPState = 'confirm' this.setupState.setupOTPState = 'confirm'
}) })
@ -120,18 +129,17 @@ const Mfa = {
doConfirmOTP() { doConfirmOTP() {
// handler confirm enable OTP // handler confirm enable OTP
this.error = null this.error = null
this.backendInteractor mfaConfirmOTP({
.mfaConfirmOTP({ token: this.otpConfirmToken,
token: this.otpConfirmToken, password: this.currentPassword,
password: this.currentPassword, credentials: useOAuthStore().token,
}) })
.then((res) => { .then(() => {
if (res.error) {
this.error = res.error
return
}
this.completeSetup() this.completeSetup()
}) })
.catch((error) => {
this.error = error
})
}, },
completeSetup() { completeSetup() {
@ -152,7 +160,9 @@ const Mfa = {
// fetch settings from server // fetch settings from server
async fetchSettings() { async fetchSettings() {
const result = await this.backendInteractor.settingsMFA() const { data: result } = await settingsMFA({
credentials: useOAuthStore().token,
})
if (result.error) return if (result.error) return
this.settings = result.settings this.settings = result.settings
this.settings.available = true this.settings.available = true

View file

@ -1,7 +1,9 @@
import { mapState } from 'vuex'
import Confirm from './confirm.vue' import Confirm from './confirm.vue'
import { useOAuthStore } from 'src/stores/oauth.js'
import { mfaDisableOTP } from 'src/api/user.js'
export default { export default {
props: ['settings'], props: ['settings'],
data: () => ({ data: () => ({
@ -17,9 +19,6 @@ export default {
isActivated() { isActivated() {
return this.settings.totp return this.settings.totp
}, },
...mapState({
backendInteractor: (state) => state.api.backendInteractor,
}),
}, },
methods: { methods: {
doActivate() { doActivate() {
@ -36,19 +35,18 @@ export default {
// confirm deactivate TOTP method // confirm deactivate TOTP method
this.error = null this.error = null
this.inProgress = true this.inProgress = true
this.backendInteractor mfaDisableOTP({
.mfaDisableOTP({ password: this.currentPassword,
password: this.currentPassword, credentials: useOAuthStore().token,
}) }).then(({ data: res }) => {
.then((res) => { this.inProgress = false
this.inProgress = false if (res.error) {
if (res.error) { this.error = res.error
this.error = res.error return
return }
} this.deactivate = false
this.deactivate = false this.$emit('deactivate')
this.$emit('deactivate') })
})
}, },
}, },
} }

View file

@ -2,10 +2,19 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Mfa from './mfa.vue' import Mfa from './mfa.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useOAuthTokensStore } from 'src/stores/oauth_tokens' 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' import localeService from 'src/services/locale/locale.service.js'
const SecurityTab = { const SecurityTab = {
@ -65,78 +74,79 @@ const SecurityTab = {
this.deletingAccount = true this.deletingAccount = true
}, },
deleteAccount() { deleteAccount() {
this.$store.state.api.backendInteractor deleteAccount({
.deleteAccount({ password: this.deleteAccountConfirmPasswordInput }) credentials: useOAuthStore().token,
.then((res) => { password: this.deleteAccountConfirmPasswordInput,
if (res.status === 'success') { }).then(({ data: res }) => {
this.$store.dispatch('logout') if (res.status === 'success') {
this.$router.push({ name: 'root' }) this.$store.dispatch('logout')
} else { this.$router.push({ name: 'root' })
this.deleteAccountError = res.error } else {
} this.deleteAccountError = res.error
}) }
})
}, },
changePassword() { changePassword() {
const params = { const params = {
password: this.changePasswordInputs[0], password: this.changePasswordInputs[0],
newPassword: this.changePasswordInputs[1], newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2], newPasswordConfirmation: this.changePasswordInputs[2],
credentials: useOAuthStore().token,
} }
this.$store.state.api.backendInteractor changePassword(params).then(({ data: res }) => {
.changePassword(params) if (res.status === 'success') {
.then((res) => { this.changedPassword = true
if (res.status === 'success') { this.changePasswordError = false
this.changedPassword = true this.logout()
this.changePasswordError = false } else {
this.logout() this.changedPassword = false
} else { this.changePasswordError = res.error
this.changedPassword = false }
this.changePasswordError = res.error })
}
})
}, },
changeEmail() { changeEmail() {
const params = { const params = {
email: this.newEmail, email: this.newEmail,
password: this.changeEmailPassword, password: this.changeEmailPassword,
credentials: useOAuthStore().token,
} }
this.$store.state.api.backendInteractor changeEmail(params).then(({ data: res }) => {
.changeEmail(params) if (res.status === 'success') {
.then((res) => { this.changedEmail = true
if (res.status === 'success') { this.changeEmailError = false
this.changedEmail = true } else {
this.changeEmailError = false this.changedEmail = false
} else { this.changeEmailError = res.error
this.changedEmail = false }
this.changeEmailError = res.error })
}
})
}, },
moveAccount() { moveAccount() {
const params = { const params = {
targetAccount: this.moveAccountTarget, targetAccount: this.moveAccountTarget,
password: this.moveAccountPassword, password: this.moveAccountPassword,
credentials: useOAuthStore().token,
} }
this.$store.state.api.backendInteractor moveAccount(params).then(({ data: res }) => {
.moveAccount(params) if (res.status === 'success') {
.then((res) => { this.movedAccount = true
if (res.status === 'success') { this.moveAccountError = false
this.movedAccount = true } else {
this.moveAccountError = false this.movedAccount = false
} else { this.moveAccountError = res.error
this.movedAccount = false }
this.moveAccountError = res.error })
}
})
}, },
removeAlias(alias) { removeAlias(alias) {
this.$store.state.api.backendInteractor deleteAlias({
.deleteAlias({ alias }) alias,
.then(() => this.fetchAliases()) credentials: useOAuthStore().token,
}).then(() => this.fetchAliases())
}, },
addAlias() { addAlias() {
this.$store.state.api.backendInteractor addAlias({
.addAlias({ alias: this.addAliasTarget }) alias: this.addAliasTarget,
credentials: useOAuthStore().token,
})
.then(() => { .then(() => {
this.addedAlias = true this.addedAlias = true
this.addAliasError = false this.addAliasError = false
@ -149,9 +159,10 @@ const SecurityTab = {
.then(() => this.fetchAliases()) .then(() => this.fetchAliases())
}, },
fetchAliases() { fetchAliases() {
this.$store.state.api.backendInteractor listAliases({
.listAliases() credentials: useOAuthStore().token,
.then((res) => { })
.then(({ data: res }) => {
this.aliases = res.aliases this.aliases = res.aliases
this.listAliasesError = false this.listAliasesError = false
}) })

View file

@ -1,5 +1,4 @@
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'

View file

@ -107,7 +107,7 @@ const StatusActionButtons = {
button button
.action?.(this.funcArg) .action?.(this.funcArg)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch((err) => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err))
}, },
onExtraClose() { onExtraClose() {
this.showPin = false this.showPin = false

View file

@ -1,6 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import Attachment from 'src/components/attachment/attachment.vue' import Attachment from 'src/components/attachment/attachment.vue'
import Gallery from 'src/components/gallery/gallery.vue' import Gallery from 'src/components/gallery/gallery.vue'

View file

@ -1,7 +1,6 @@
import { find } from 'lodash' import { find } from 'lodash'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import Status from 'src/components/status/status.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'

View file

@ -4,7 +4,6 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import TabSwitcher from '../tab_switcher/tab_switcher.jsx' import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js'
const StickerPicker = { const StickerPicker = {
components: { components: {

View file

@ -2,6 +2,7 @@ import Popover from 'components/popover/popover.vue'
import SelectComponent from 'components/select/select.vue' import SelectComponent from 'components/select/select.vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { useAdminSettingsStore } from 'src/stores/admin_settings'
import { useEmojiStore } from 'src/stores/emoji' import { useEmojiStore } from 'src/stores/emoji'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
@ -37,7 +38,7 @@ export default {
}) })
}, },
copyToLocalPack() { copyToLocalPack() {
this.$store.state.api.backendInteractor useAdminSettingsStore()
.addNewEmojiFile({ .addNewEmojiFile({
packName: this.packName, packName: this.packName,
file: this.$attrs.src, file: this.$attrs.src,

View file

@ -1,14 +1,11 @@
// eslint-disable-next-line no-unused // eslint-disable-next-line no-unused
import { mapState } from 'pinia' import { Fragment } from 'vue'
import { Fragment, h } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './tab_switcher.scss' import './tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface.js'
const findFirstUsable = (slots) => slots.findIndex((_) => _.props) const findFirstUsable = (slots) => slots.findIndex((_) => _.props)
export default { export default {

View file

@ -25,6 +25,7 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { usePostStatusStore } from 'src/stores/post_status' import { usePostStatusStore } from 'src/stores/post_status'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' 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 { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
import localeService from 'src/services/locale/locale.service.js' import localeService from 'src/services/locale/locale.service.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -597,9 +598,8 @@ export default {
params.header = this.newBannerFile params.header = this.newBannerFile
} }
this.$store.state.api.backendInteractor updateProfile({ params })
.updateProfile({ params }) .then(({ data: user }) => {
.then((user) => {
this.newFields.splice(this.newFields.length) this.newFields.splice(this.newFields.length)
merge(this.newFields, user.fields) merge(this.newFields, user.fields)
this.$store.commit('addNewUsers', [user]) this.$store.commit('addNewUsers', [user])

View file

@ -1,5 +1,3 @@
import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'

View file

@ -1,5 +1,4 @@
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import UserCard from 'src/components/user_card/user_card.vue' import UserCard from 'src/components/user_card/user_card.vue'

View file

@ -1,5 +1,4 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia'
import FollowCard from 'src/components/follow_card/follow_card.vue' import FollowCard from 'src/components/follow_card/follow_card.vue'
import List from 'src/components/list/list.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 Timeline from 'src/components/timeline/timeline.vue'
import UserCard from 'src/components/user_card/user_card.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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'

View file

@ -1,6 +1,3 @@
import { get } from 'lodash'
import { mapState } from 'pinia'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import Status from 'src/components/status/status.vue' import Status from 'src/components/status/status.vue'

View file

@ -5,8 +5,11 @@ import List from 'src/components/list/list.vue'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import UserLink from 'src/components/user_link/user_link.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 { useReportsStore } from 'src/stores/reports.js'
import { reportUser } from 'src/api/user.js'
const UserReportingModal = { const UserReportingModal = {
components: { components: {
List, List,
@ -70,9 +73,9 @@ const UserReportingModal = {
comment: this.comment, comment: this.comment,
forward: this.forward, forward: this.forward,
statusIds: [...this.statusIdsToReport], statusIds: [...this.statusIdsToReport],
credentials: useOAuthStore().token,
} }
this.$store.state.api.backendInteractor reportUser({ ...params })
.reportUser({ ...params })
.then(() => { .then(() => {
this.processing = false this.processing = false
this.resetState() this.resetState()

View file

@ -1,7 +1,8 @@
import FollowCard from 'src/components/follow_card/follow_card.vue' 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 = { const WhoToFollow = {
components: { components: {
@ -17,21 +18,22 @@ const WhoToFollow = {
}, },
methods: { methods: {
showWhoToFollow(reply) { showWhoToFollow(reply) {
reply.forEach((i) => { reply.forEach(({ id }) => {
this.$store.state.api.backendInteractor fetchUser({
.fetchUser({ id: i.acct }) id,
.then((externalUser) => { credentials: useOAuthStore().token,
if (!externalUser.error) { }).then(({ data: externalUser }) => {
this.$store.commit('addNewUsers', [externalUser]) if (!externalUser.error) {
this.users.push(externalUser) this.$store.commit('addNewUsers', [externalUser])
} this.users.push(externalUser)
}) }
})
}) })
}, },
getWhoToFollow() { getWhoToFollow() {
const credentials = this.$store.state.users.currentUser.credentials const credentials = useOAuthStore().token
if (credentials) { if (credentials) {
apiService.suggestions({ credentials }).then((reply) => { suggestions({ credentials }).then(({ data: reply }) => {
this.showWhoToFollow(reply) this.showWhoToFollow(reply)
}) })
} }

View file

@ -1,10 +1,10 @@
import { shuffle } from 'lodash' import { shuffle } from 'lodash'
import apiService from '../../services/api/api.service.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.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' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
function showWhoToFollow(panel, reply) { function showWhoToFollow(panel, reply) {
@ -18,14 +18,15 @@ function showWhoToFollow(panel, reply) {
toFollow.img = img toFollow.img = img
toFollow.name = name toFollow.name = name
panel.$store.state.api.backendInteractor fetchUser({
.fetchUser({ id: name }) id: name,
.then((externalUser) => { credentials: useOAuthStore().token,
if (!externalUser.error) { }).then(({ data: externalUser }) => {
panel.$store.commit('addNewUsers', [externalUser]) if (!externalUser.error) {
toFollow.id = externalUser.id panel.$store.commit('addNewUsers', [externalUser])
} toFollow.id = externalUser.id
}) }
})
}) })
} }
@ -35,7 +36,7 @@ function getWhoToFollow(panel) {
panel.usersToFollow.forEach((toFollow) => { panel.usersToFollow.forEach((toFollow) => {
toFollow.name = 'Loading...' toFollow.name = 'Loading...'
}) })
apiService.suggestions({ credentials }).then((reply) => { suggestions({ credentials }).then(({ data: reply }) => {
showWhoToFollow(panel, reply) showWhoToFollow(panel, reply)
}) })
} }

View file

@ -1,5 +1,3 @@
import Cookies from 'js-cookie'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useI18nStore } from 'src/stores/i18n.js' import { useI18nStore } from 'src/stores/i18n.js'

View file

@ -1,20 +1,27 @@
import { Socket } from 'phoenix' 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 { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useShoutStore } from 'src/stores/shout.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 retryTimeout = (multiplier) => 1000 * multiplier
const api = { const api = {
state: { state: {
retryMultiplier: 1, retryMultiplier: 1,
backendInteractor: backendInteractorService(),
fetchers: {}, fetchers: {},
socket: null, socket: null,
mastoUserSocket: null, mastoUserSocket: null,
@ -25,9 +32,6 @@ const api = {
followRequestCount: (state) => state.followRequests.length, followRequestCount: (state) => state.followRequests.length,
}, },
mutations: { mutations: {
setBackendInteractor(state, backendInteractor) {
state.backendInteractor = backendInteractor
},
addFetcher(state, { fetcherName, fetcher }) { addFetcher(state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher state.fetchers[fetcherName] = fetcher
}, },
@ -91,9 +95,16 @@ const api = {
try { try {
const { state, commit, dispatch, rootState } = store const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends 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( state.mastoUserSocket.addEventListener(
'pleroma:authenticated', 'pleroma:authenticated',
() => { () => {
@ -245,7 +256,7 @@ const api = {
return return
if (store.state.fetchers[timeline]) return if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetchingTimeline({ const fetcher = timelineFetcher.startFetching({
timeline, timeline,
store, store,
userId, userId,
@ -253,7 +264,9 @@ const api = {
statusId, statusId,
bookmarkFolderId, bookmarkFolderId,
tag, tag,
credentials: useOAuthStore().token,
}) })
store.commit('addFetcher', { fetcherName: timeline, fetcher }) store.commit('addFetcher', { fetcherName: timeline, fetcher })
}, },
stopFetchingTimeline(store, timeline) { stopFetchingTimeline(store, timeline) {
@ -261,19 +274,22 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher }) store.commit('removeFetcher', { fetcherName: timeline, fetcher })
}, },
fetchTimeline(store, { timeline, ...rest }) { fetchTimeline(store, { timeline, ...rest }) {
store.state.backendInteractor.fetchTimeline({ fetchTimeline({
store, store,
timeline, timeline,
...rest, ...rest,
credentials: useOAuthStore().token,
}) })
}, },
// Notifications // Notifications
startFetchingNotifications(store) { startFetchingNotifications(store) {
if (store.state.fetchers.notifications) return if (store.state.fetchers.notifications) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ const fetcher = notificationsFetcher.startFetching({
store, store,
credentials: useOAuthStore().token,
}) })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
}, },
@ -282,19 +298,14 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
}, },
fetchNotifications(store, { ...rest }) {
store.state.backendInteractor.fetchNotifications({
store,
...rest,
})
},
// Follow requests // Follow requests
startFetchingFollowRequests(store) { startFetchingFollowRequests(store) {
if (store.state.fetchers.followRequests) return if (store.state.fetchers.followRequests) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests( const fetcher = followRequestFetcher.startFetchingFollowRequests({
{ store }, store,
) credentials: useOAuthStore().token,
})
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
}, },
@ -303,39 +314,6 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher }) 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 // Pleroma websocket
setWsToken(store, token) { setWsToken(store, token) {

View file

@ -9,6 +9,10 @@ import {
} from '../services/entity_normalizer/entity_normalizer.service.js' } from '../services/entity_normalizer/entity_normalizer.service.js'
import { promiseInterval } from '../services/promise_interval/promise_interval.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 = () => ({ const emptyChatList = () => ({
data: [], data: [],
idStore: {}, idStore: {},
@ -36,7 +40,7 @@ const unreadChatCount = (state) => {
return sumBy(state.chatList.data, 'unread') return sumBy(state.chatList.data, 'unread')
} }
const chats = { const chatsModule = {
state: { ...defaultState }, state: { ...defaultState },
getters: { getters: {
currentChat: (state) => state.openedChats[state.currentChatId], currentChat: (state) => state.openedChats[state.currentChatId],
@ -51,7 +55,6 @@ const chats = {
// Chat list // Chat list
startFetchingChats({ dispatch, commit }) { startFetchingChats({ dispatch, commit }) {
const fetcher = () => dispatch('fetchChats', { latest: true }) const fetcher = () => dispatch('fetchChats', { latest: true })
fetcher()
commit('setChatListFetcher', { commit('setChatListFetcher', {
fetcher: () => promiseInterval(fetcher, 5000), fetcher: () => promiseInterval(fetcher, 5000),
}) })
@ -60,8 +63,10 @@ const chats = {
commit('setChatListFetcher', { fetcher: undefined }) commit('setChatListFetcher', { fetcher: undefined })
}, },
fetchChats({ dispatch, rootState }) { fetchChats({ dispatch, rootState }) {
return rootState.api.backendInteractor.chats().then(({ chats }) => { return chats({
dispatch('addNewChats', { chats }) credentials: useOAuthStore().token,
}).then(({ chatList }) => {
dispatch('addNewChats', { chats: chatList })
return chats return chats
}) })
}, },
@ -113,11 +118,18 @@ const chats = {
commit('readChat', { id, lastReadId }) commit('readChat', { id, lastReadId })
if (isNewMessage) { if (isNewMessage) {
rootState.api.backendInteractor.readChat({ id, lastReadId }) readChat({
id,
lastReadId,
credentials: useOAuthStore().token,
})
} }
}, },
deleteChatMessage({ rootState, commit }, value) { deleteChatMessage({ rootState, commit }, value) {
rootState.api.backendInteractor.deleteChatMessage(value) deleteChatMessage({
...value,
credentials: useOAuthStore().token,
})
commit('deleteChatMessage', { commit, ...value }) commit('deleteChatMessage', { commit, ...value })
}, },
resetChats({ commit, dispatch }) { resetChats({ commit, dispatch }) {
@ -262,4 +274,4 @@ const chats = {
}, },
} }
export default chats export default chatsModule

View file

@ -1,4 +1,4 @@
import { get, set } from 'lodash' import { get } from 'lodash'
const browserLocale = (navigator.language || 'en').split('-')[0] const browserLocale = (navigator.language || 'en').split('-')[0]

View file

@ -1,4 +1,3 @@
import apiService from '../services/api/api.service.js'
import { import {
closeAllDesktopNotifications, closeAllDesktopNotifications,
closeDesktopNotification, closeDesktopNotification,
@ -11,9 +10,12 @@ import {
import { useI18nStore } from 'src/stores/i18n.js' import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useReportsStore } from 'src/stores/reports.js' import { useReportsStore } from 'src/stores/reports.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js'
const emptyNotifications = () => ({ const emptyNotifications = () => ({
desktopNotificationSilence: true, desktopNotificationSilence: true,
maxId: 0, maxId: 0,
@ -154,33 +156,32 @@ export const notifications = {
}, },
markNotificationsAsSeen({ rootState, state, commit }) { markNotificationsAsSeen({ rootState, state, commit }) {
commit('markNotificationsAsSeen') commit('markNotificationsAsSeen')
apiService markNotificationsAsSeen({
.markNotificationsAsSeen({ id: state.maxId,
id: state.maxId, credentials: rootState.users.currentUser.credentials,
credentials: rootState.users.currentUser.credentials, }).then(() => {
}) closeAllDesktopNotifications(rootState)
.then(() => { })
closeAllDesktopNotifications(rootState)
})
}, },
markSingleNotificationAsSeen({ rootState, commit }, { id }) { markSingleNotificationAsSeen({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id }) commit('markSingleNotificationAsSeen', { id })
apiService markNotificationsAsSeen({
.markNotificationsAsSeen({ single: true,
single: true, id,
id, credentials: rootState.users.currentUser.credentials,
credentials: rootState.users.currentUser.credentials, }).then(() => {
}) closeDesktopNotification(rootState, { id })
.then(() => { })
closeDesktopNotification(rootState, { id })
})
}, },
dismissNotificationLocal({ commit }, { id }) { dismissNotificationLocal({ commit }, { id }) {
commit('dismissNotification', { id }) commit('dismissNotification', { id })
}, },
dismissNotification({ rootState, commit }, { id }) { dismissNotification({ rootState, commit }, { id }) {
commit('dismissNotification', { id }) commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id }) dismissNotification({
id,
credentials: useOAuthStore().token,
})
}, },
updateNotification({ commit }, { id, updater }) { updateNotification({ commit }, { id, updater }) {
commit('updateNotification', { id, updater }) commit('updateNotification', { id, updater })

View file

@ -1,28 +1,34 @@
import { get, set } from 'lodash' 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 defaultApi = ({ rootState, commit }, { path, value }) => {
const params = {} const params = {}
set(params, path, value) set(params, path, value)
return rootState.api.backendInteractor return updateProfile({
.updateProfile({ params }) params,
.then((result) => { credentials: useOAuthStore().token,
commit('addNewUsers', [result]) }).then(({ data: result }) => {
commit('setCurrentUser', result) commit('addNewUsers', [result])
}) commit('setCurrentUser', result)
})
} }
const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
const settings = {} const settings = {}
set(settings, path, value) set(settings, path, value)
return rootState.api.backendInteractor return updateNotificationSettings({
.updateNotificationSettings({ settings }) settings,
.then((result) => { credentials: useOAuthStore().token,
if (result.status === 'success') { }).then(({ data: result }) => {
commit('confirmProfileOption', { name, value }) if (result.status === 'success') {
} else { commit('confirmProfileOption', { name, value })
commit('confirmProfileOption', { name, value: oldValue }) } else {
} commit('confirmProfileOption', { name, value: oldValue })
}) }
})
} }
/** /**

View file

@ -13,10 +13,36 @@ import {
slice, slice,
} from 'lodash' } from 'lodash'
import apiService from '../services/api/api.service.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.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) => ({ const emptyTl = (userId = 0) => ({
statuses: [], statuses: [],
@ -131,9 +157,8 @@ const getLatestScrobble = (state, user) => {
state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000 state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
if (!scrobblesSupport) return if (!scrobblesSupport) return
apiService fetchScrobbles({ accountId: user.id })
.fetchScrobbles({ accountId: user.id }) .then(({ data: scrobbles }) => {
.then((scrobbles) => {
if (scrobbles?.error) { if (scrobbles?.error) {
useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false) useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false)
return return
@ -602,25 +627,24 @@ const statuses = {
}) })
}, },
fetchStatus({ rootState, dispatch }, id) { fetchStatus({ rootState, dispatch }, id) {
return rootState.api.backendInteractor return fetchStatus({ id }).then(({ data: status }) =>
.fetchStatus({ id }) dispatch('addNewStatuses', { statuses: [status] }),
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) )
}, },
fetchStatusSource({ rootState }, status) { fetchStatusSource({ rootState }, status) {
return apiService.fetchStatusSource({ return fetchStatusSource({
id: status.id, id: status.id,
credentials: rootState.users.currentUser.credentials, credentials: useOAuthStore().token,
}) }).then(({ data }) => data)
}, },
fetchStatusHistory(_, status) { fetchStatusHistory(_, status) {
return apiService.fetchStatusHistory({ status }) return fetchStatusHistory({ status }).then(({ data }) => data)
}, },
deleteStatus({ rootState, commit }, status) { deleteStatus({ rootState, commit }, status) {
apiService deleteStatus({
.deleteStatus({ id: status.id,
id: status.id, credentials: useOAuthStore().token,
credentials: rootState.users.currentUser.credentials, })
})
.then(() => { .then(() => {
commit('setDeleted', { status }) commit('setDeleted', { status })
}) })
@ -643,99 +667,115 @@ const statuses = {
favorite({ rootState, commit }, status) { favorite({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) commit('setFavorited', { status, value: true })
rootState.api.backendInteractor favorite({
.favorite({ id: status.id }) id: status.id,
.then((status) => credentials: useOAuthStore().token,
commit('setFavoritedConfirm', { }).then(({ data: status }) =>
status, commit('setFavoritedConfirm', {
user: rootState.users.currentUser, status,
}), user: rootState.users.currentUser,
) }),
)
}, },
unfavorite({ rootState, commit }, status) { unfavorite({ rootState, commit }, status) {
// Optimistic unfavoriting... // Optimistic unfavoriting...
commit('setFavorited', { status, value: false }) commit('setFavorited', { status, value: false })
rootState.api.backendInteractor unfavorite({
.unfavorite({ id: status.id }) id: status.id,
.then((status) => credentials: useOAuthStore().token,
commit('setFavoritedConfirm', { }).then(({ data: status }) =>
status, commit('setFavoritedConfirm', {
user: rootState.users.currentUser, status,
}), user: rootState.users.currentUser,
) }),
)
}, },
fetchPinnedStatuses({ rootState, dispatch }, userId) { fetchPinnedStatuses({ rootState, dispatch }, userId) {
rootState.api.backendInteractor fetchPinnedStatuses({
.fetchPinnedStatuses({ id: userId }) id: userId,
.then((statuses) => credentials: useOAuthStore().token,
dispatch('addNewStatuses', { }).then(({ data: statuses }) =>
statuses, dispatch('addNewStatuses', {
timeline: 'user', statuses,
userId, timeline: 'user',
showImmediately: true, userId,
noIdUpdate: true, showImmediately: true,
}), noIdUpdate: true,
) }),
)
}, },
pinStatus({ rootState, dispatch }, statusId) { pinStatus({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor return pinOwnStatus({
.pinOwnStatus({ id: statusId }) id: statusId,
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) credentials: useOAuthStore().token,
}).then(({ data: status }) =>
dispatch('addNewStatuses', { statuses: [status] }),
)
}, },
unpinStatus({ rootState, dispatch }, statusId) { unpinStatus({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor return unpinOwnStatus({
.unpinOwnStatus({ id: statusId }) id: statusId,
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) credentials: useOAuthStore().token,
}).then(({ data: status }) =>
dispatch('addNewStatuses', { statuses: [status] }),
)
}, },
muteConversation({ rootState, commit }, { id: statusId }) { muteConversation({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor return muteConversation({
.muteConversation({ id: statusId }) id: statusId,
.then((status) => commit('setMutedStatus', status)) credentials: useOAuthStore().token,
}).then(({ data: status }) => commit('setMutedStatus', status))
}, },
unmuteConversation({ rootState, commit }, { id: statusId }) { unmuteConversation({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor return unmuteConversation({
.unmuteConversation({ id: statusId }) id: statusId,
.then((status) => commit('setMutedStatus', status)) credentials: useOAuthStore().token,
}).then(({ data: status }) => commit('setMutedStatus', status))
}, },
retweet({ rootState, commit }, status) { retweet({ rootState, commit }, status) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor retweet({
.retweet({ id: status.id }) id: status.id,
.then((status) => credentials: useOAuthStore().token,
commit('setRetweetedConfirm', { }).then(({ data: status }) =>
status: status.retweeted_status, commit('setRetweetedConfirm', {
user: rootState.users.currentUser, status: status.retweeted_status,
}), user: rootState.users.currentUser,
) }),
)
}, },
unretweet({ rootState, commit }, status) { unretweet({ rootState, commit }, status) {
// Optimistic unretweeting... // Optimistic unretweeting...
commit('setRetweeted', { status, value: false }) commit('setRetweeted', { status, value: false })
rootState.api.backendInteractor unretweet({
.unretweet({ id: status.id }) id: status.id,
.then((status) => credentials: useOAuthStore().token,
commit('setRetweetedConfirm', { }).then(({ data: status }) =>
status, commit('setRetweetedConfirm', {
user: rootState.users.currentUser, status,
}), user: rootState.users.currentUser,
) }),
)
}, },
bookmark({ rootState, commit }, status) { bookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: true }) commit('setBookmarked', { status, value: true })
rootState.api.backendInteractor bookmarkStatus({
.bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id }) id: status.id,
.then((status) => { folder_id: status.bookmark_folder_id,
commit('setBookmarkedConfirm', { status }) credentials: useOAuthStore().token,
}) }).then(({ data: status }) => {
commit('setBookmarkedConfirm', { status })
})
}, },
unbookmark({ rootState, commit }, status) { unbookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: false }) commit('setBookmarked', { status, value: false })
rootState.api.backendInteractor unbookmarkStatus({
.unbookmarkStatus({ id: status.id }) id: status.id,
.then((status) => { credentials: useOAuthStore().token,
commit('setBookmarkedConfirm', { status }) }).then(({ data: status }) => {
}) commit('setBookmarkedConfirm', { status })
})
}, },
queueFlush({ commit }, { timeline, id }) { queueFlush({ commit }, { timeline, id }) {
commit('queueFlush', { timeline, id }) commit('queueFlush', { timeline, id })
@ -745,8 +785,14 @@ const statuses = {
}, },
fetchFavsAndRepeats({ rootState, commit }, id) { fetchFavsAndRepeats({ rootState, commit }, id) {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), fetchFavoritedByUsers({
rootState.api.backendInteractor.fetchRebloggedByUsers({ id }), id,
credentials: useOAuthStore().token,
}).then(({ data }) => data),
fetchRebloggedByUsers({
id,
credentials: useOAuthStore().token,
}).then(({ data }) => data),
]).then(([favoritedByUsers, rebloggedByUsers]) => { ]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { commit('addFavs', {
id, id,
@ -765,7 +811,11 @@ const statuses = {
if (!currentUser) return if (!currentUser) return
commit('addOwnReaction', { id, emoji, currentUser }) commit('addOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(() => { reactWithEmoji({
id,
emoji,
credentials: useOAuthStore().token,
}).then(() => {
dispatch('fetchEmojiReactionsBy', id) dispatch('fetchEmojiReactionsBy', id)
}) })
}, },
@ -774,59 +824,70 @@ const statuses = {
if (!currentUser) return if (!currentUser) return
commit('removeOwnReaction', { id, emoji, currentUser }) commit('removeOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor unreactWithEmoji({
.unreactWithEmoji({ id, emoji }) id,
.then(() => { emoji,
dispatch('fetchEmojiReactionsBy', id) currentUser: rootState.users.currentUser,
}) }).then(() => {
dispatch('fetchEmojiReactionsBy', id)
})
}, },
fetchEmojiReactionsBy({ rootState, commit }, id) { fetchEmojiReactionsBy({ rootState, commit }, id) {
return rootState.api.backendInteractor return fetchEmojiReactions({
.fetchEmojiReactions({ id }) id,
.then((emojiReactions) => { credentials: useOAuthStore().token,
commit('addEmojiReactionsBy', { }).then(({ data: emojiReactions }) => {
id, commit('addEmojiReactionsBy', {
emojiReactions, id,
currentUser: rootState.users.currentUser, emojiReactions,
}) currentUser: rootState.users.currentUser,
}) })
})
}, },
fetchFavs({ rootState, commit }, id) { fetchFavs({ rootState, commit }, id) {
rootState.api.backendInteractor fetchFavoritedByUsers({
.fetchFavoritedByUsers({ id }) id,
.then((favoritedByUsers) => credentials: useOAuthStore().token,
commit('addFavs', { }).then(({ data: favoritedByUsers }) =>
id, commit('addFavs', {
favoritedByUsers, id,
currentUser: rootState.users.currentUser, favoritedByUsers,
}), currentUser: rootState.users.currentUser,
) }),
)
}, },
fetchRepeats({ rootState, commit }, id) { fetchRepeats({ rootState, commit }, id) {
rootState.api.backendInteractor fetchRebloggedByUsers({
.fetchRebloggedByUsers({ id }) id,
.then((rebloggedByUsers) => credentials: useOAuthStore().token,
commit('addRepeats', { }).then(({ data: rebloggedByUsers }) =>
id, commit('addRepeats', {
rebloggedByUsers, id,
currentUser: rootState.users.currentUser, rebloggedByUsers,
}), currentUser: rootState.users.currentUser,
) }),
)
}, },
search(store, { q, resolve, limit, offset, following, type }) { search(store, { q, resolve, limit, offset, following, type }) {
return store.rootState.api.backendInteractor return search2({
.search2({ q, resolve, limit, offset, following, type }) q,
.then((data) => { resolve,
store.commit('addNewUsers', data.accounts) limit,
store.commit( offset,
'addNewUsers', following,
data.statuses.map((s) => s.user).filter((u) => u), type,
) credentials: useOAuthStore().token,
data.statuses = store.commit('addNewStatuses', { }).then(({ data }) => {
statuses: data.statuses, store.commit('addNewUsers', data.accounts)
}) store.commit(
return data 'addNewUsers',
data.statuses.map((s) => s.user).filter((u) => u),
)
data.statuses = store.commit('addNewStatuses', {
statuses: data.statuses,
}) })
return data
})
}, },
setVirtualHeight({ commit }, { statusId, height }) { setVirtualHeight({ commit }, { statusId, height }) {
commit('setVirtualHeight', { statusId, height }) commit('setVirtualHeight', { statusId, height })

View file

@ -1,3 +1,4 @@
import Cookies from 'js-cookie'
import { import {
compact, compact,
concat, concat,
@ -9,9 +10,6 @@ import {
uniq, uniq,
} from 'lodash' } 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 { import {
registerPushNotifications, registerPushNotifications,
unregisterPushNotifications, unregisterPushNotifications,
@ -21,15 +19,42 @@ import {
windowWidth, windowWidth,
} from '../services/window_utils/window_utils' } 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 { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useListsStore } from 'src/stores/lists.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.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 // TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => { export const mergeOrAdd = (arr, obj, item) => {
if (!item) { if (!item) {
@ -72,43 +97,35 @@ const blockUser = (store, args) => {
store.commit('updateUserRelationship', [predictedRelationship]) store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addBlockId', id) store.commit('addBlockId', id)
return store.rootState.api.backendInteractor return apiBlockUser({ id, expiresIn }).then(({ data: relationship }) => {
.blockUser({ id, expiresIn }) store.commit('updateUserRelationship', [relationship])
.then((relationship) => { store.commit('addBlockId', id)
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id }) store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id }) store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { store.commit('removeStatus', {
timeline: 'publicAndExternal', timeline: 'publicAndExternal',
userId: id, userId: id,
})
}) })
})
} }
const unblockUser = (store, id) => { const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor return apiUnblockUser({ id }).then(({ data: relationship }) =>
.unblockUser({ id }) store.commit('updateUserRelationship', [relationship]),
.then((relationship) => )
store.commit('updateUserRelationship', [relationship]),
)
} }
const removeUserFromFollowers = (store, id) => { const removeUserFromFollowers = (store, id) => {
return store.rootState.api.backendInteractor return removeUserFromFollowers({ id }).then((relationship) =>
.removeUserFromFollowers({ id }) store.commit('updateUserRelationship', [relationship]),
.then((relationship) => )
store.commit('updateUserRelationship', [relationship]),
)
} }
const editUserNote = (store, { id, comment }) => { const editUserNote = (store, { id, comment }) => {
return store.rootState.api.backendInteractor return editUserNote({ id, comment }).then((relationship) =>
.editUserNote({ id, comment }) store.commit('updateUserRelationship', [relationship]),
.then((relationship) => )
store.commit('updateUserRelationship', [relationship]),
)
} }
const muteUser = (store, args) => { const muteUser = (store, args) => {
@ -119,12 +136,14 @@ const muteUser = (store, args) => {
store.commit('updateUserRelationship', [predictedRelationship]) store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)
return store.rootState.api.backendInteractor return apiMuteUser({
.muteUser({ id, expiresIn }) id,
.then((relationship) => { expiresIn,
store.commit('updateUserRelationship', [relationship]) credentials: useOAuthStore().token,
store.commit('addMuteId', id) }).then(({ data: relationship }) => {
}) store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
})
} }
const unmuteUser = (store, id) => { const unmuteUser = (store, id) => {
@ -132,39 +151,43 @@ const unmuteUser = (store, id) => {
predictedRelationship.muting = false predictedRelationship.muting = false
store.commit('updateUserRelationship', [predictedRelationship]) store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor return apiUnmuteUser({ id }).then(({ data: relationship }) =>
.unmuteUser({ id }) store.commit('updateUserRelationship', [relationship]),
.then((relationship) => )
store.commit('updateUserRelationship', [relationship]),
)
} }
const hideReblogs = (store, userId) => { const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor return followUser({
.followUser({ id: userId, reblogs: false }) id: userId,
.then((relationship) => { reblogs: false,
store.commit('updateUserRelationship', [relationship]) credentials: useOAuthStore().token,
}) }).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
} }
const showReblogs = (store, userId) => { const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor return followUser({
.followUser({ id: userId, reblogs: true }) id: userId,
.then((relationship) => reblogs: true,
store.commit('updateUserRelationship', [relationship]), credentials: useOAuthStore().token,
) }).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
} }
const muteDomain = (store, domain) => { const muteDomain = (store, domain) => {
return store.rootState.api.backendInteractor return muteDomain({
.muteDomain({ domain }) domain,
.then(() => store.commit('addDomainMute', domain)) credentials: useOAuthStore().token,
}).then(() => store.commit('addDomainMute', domain))
} }
const unmuteDomain = (store, domain) => { const unmuteDomain = (store, domain) => {
return store.rootState.api.backendInteractor return unmuteDomain({
.unmuteDomain({ domain }) domain,
.then(() => store.commit('removeDomainMute', domain)) credentials: useOAuthStore().token,
}).then(() => store.commit('removeDomainMute', domain))
} }
export const mutations = { export const mutations = {
@ -385,55 +408,70 @@ const users = {
}) })
}, },
fetchUser(store, id) { fetchUser(store, id) {
return store.rootState.api.backendInteractor return fetchUser({
.fetchUser({ id }) id,
.then((user) => { credentials: useOAuthStore().token,
})
.then(({ data: user }) => {
store.commit('addNewUsers', [user]) store.commit('addNewUsers', [user])
return user return user
}) })
.catch((error) => {
if (error.statusCode === 404) {
console.warn(`User ${id} not found`)
} else {
throw error
}
})
}, },
fetchUserByName(store, name) { fetchUserByName(store, name) {
return store.rootState.api.backendInteractor return fetchUserByName({
.fetchUserByName({ name }) name,
.then((user) => { credentials: useOAuthStore().token,
store.commit('addNewUsers', [user]) }).then(({ data: user }) => {
return user store.commit('addNewUsers', [user])
}) return user
})
}, },
fetchUserRelationship(store, id) { fetchUserRelationship(store, id) {
if (store.state.currentUser) { if (store.state.currentUser) {
store.rootState.api.backendInteractor fetchUserRelationship({
.fetchUserRelationship({ id }) id,
.then((relationships) => credentials: useOAuthStore().token,
store.commit('updateUserRelationship', relationships), }).then(({ data: relationships }) =>
) store.commit('updateUserRelationship', relationships),
)
} }
}, },
fetchUserInLists(store, id) { fetchUserInLists(store, id) {
if (store.state.currentUser) { if (store.state.currentUser) {
store.rootState.api.backendInteractor fetchUserInLists({
.fetchUserInLists({ id }) id,
.then((inLists) => store.commit('updateUserInLists', { id, inLists })) credentials: useOAuthStore().token,
}).then(({ data: inLists }) =>
store.commit('updateUserInLists', { id, inLists }),
)
} }
}, },
fetchBlocks(store, args) { fetchBlocks(store, args) {
const { reset } = args || {} const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId const maxId = store.state.currentUser.blockIdsMaxId
return store.rootState.api.backendInteractor return fetchBlocks({
.fetchBlocks({ maxId }) maxId,
.then((blocks) => { credentials: useOAuthStore().token,
if (reset) { }).then(({ data: blocks }) => {
store.commit('saveBlockIds', map(blocks, 'id')) if (reset) {
} else { store.commit('saveBlockIds', map(blocks, 'id'))
map(blocks, 'id').map((id) => store.commit('addBlockId', id)) } else {
} map(blocks, 'id').map((id) => store.commit('addBlockId', id))
if (blocks.length) { }
store.commit('setBlockIdsMaxId', last(blocks).id) if (blocks.length) {
} store.commit('setBlockIdsMaxId', last(blocks).id)
store.commit('addNewUsers', blocks) }
return blocks store.commit('addNewUsers', blocks)
}) return blocks
})
}, },
blockUser(store, data) { blockUser(store, data) {
return blockUser(store, data) return blockUser(store, data)
@ -457,20 +495,21 @@ const users = {
const { reset } = args || {} const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId const maxId = store.state.currentUser.muteIdsMaxId
return store.rootState.api.backendInteractor return fetchMutes({
.fetchMutes({ maxId }) maxId,
.then((mutes) => { credentials: useOAuthStore().token,
if (reset) { }).then(({ data: mutes }) => {
store.commit('saveMuteIds', map(mutes, 'id')) if (reset) {
} else { store.commit('saveMuteIds', map(mutes, 'id'))
map(mutes, 'id').map((id) => store.commit('addMuteId', id)) } else {
} map(mutes, 'id').map((id) => store.commit('addMuteId', id))
if (mutes.length) { }
store.commit('setMuteIdsMaxId', last(mutes).id) if (mutes.length) {
} store.commit('setMuteIdsMaxId', last(mutes).id)
store.commit('addNewUsers', mutes) }
return mutes store.commit('addNewUsers', mutes)
}) return mutes
})
}, },
muteUser(store, data) { muteUser(store, data) {
return muteUser(store, data) return muteUser(store, data)
@ -491,12 +530,12 @@ const users = {
return Promise.all(ids.map((d) => unmuteUser(store, d))) return Promise.all(ids.map((d) => unmuteUser(store, d)))
}, },
fetchDomainMutes(store) { fetchDomainMutes(store) {
return store.rootState.api.backendInteractor return fetchDomainMutes({
.fetchDomainMutes() credentials: useOAuthStore().token,
.then((domainMutes) => { }).then(({ data: domainMutes }) => {
store.commit('saveDomainMutes', domainMutes) store.commit('saveDomainMutes', domainMutes)
return domainMutes return domainMutes
}) })
}, },
muteDomain(store, domain) { muteDomain(store, domain) {
return muteDomain(store, domain) return muteDomain(store, domain)
@ -513,24 +552,28 @@ const users = {
fetchFriends({ rootState, commit }, id) { fetchFriends({ rootState, commit }, id) {
const user = rootState.users.usersObject[id] const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds) const maxId = last(user.friendIds)
return rootState.api.backendInteractor return fetchFriends({
.fetchFriends({ id, maxId }) id,
.then((friends) => { maxId,
commit('addNewUsers', friends) credentials: useOAuthStore().token,
commit('saveFriendIds', { id, friendIds: map(friends, 'id') }) }).then(({ data: friends }) => {
return friends commit('addNewUsers', friends)
}) commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
}, },
fetchFollowers({ rootState, commit }, id) { fetchFollowers({ rootState, commit }, id) {
const user = rootState.users.usersObject[id] const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds) const maxId = last(user.followerIds)
return rootState.api.backendInteractor return fetchFollowers({
.fetchFollowers({ id, maxId }) id,
.then((followers) => { maxId,
commit('addNewUsers', followers) credentials: useOAuthStore().token,
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') }) }).then(({ data: followers }) => {
return followers commit('addNewUsers', followers)
}) commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
}, },
clearFriends({ commit }, userId) { clearFriends({ commit }, userId) {
commit('clearFriends', userId) commit('clearFriends', userId)
@ -539,18 +582,22 @@ const users = {
commit('clearFollowers', userId) commit('clearFollowers', userId)
}, },
subscribeUser({ rootState, commit }, id) { subscribeUser({ rootState, commit }, id) {
return rootState.api.backendInteractor return followUser({
.followUser({ id, notify: true }) id,
.then((relationship) => notify: true,
commit('updateUserRelationship', [relationship]), credentials: useOAuthStore().token,
) }).then(({ data: relationship }) =>
commit('updateUserRelationship', [relationship]),
)
}, },
unsubscribeUser({ rootState, commit }, id) { unsubscribeUser({ rootState, commit }, id) {
return rootState.api.backendInteractor return followUser({
.followUser({ id, notify: false }) id,
.then((relationship) => notify: false,
commit('updateUserRelationship', [relationship]), credentials: useOAuthStore().token,
) }).then(({ data: relationship }) =>
commit('updateUserRelationship', [relationship]),
)
}, },
registerPushNotifications(store) { registerPushNotifications(store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
@ -611,12 +658,13 @@ const users = {
}) })
}, },
searchUsers({ rootState, commit }, { query }) { searchUsers({ rootState, commit }, { query }) {
return rootState.api.backendInteractor return searchUsers({
.searchUsers({ query }) query,
.then((users) => { credentials: useOAuthStore().token,
commit('addNewUsers', users) }).then(({ data: users }) => {
return users commit('addNewUsers', users)
}) return users
})
}, },
async signUp(store, userInfo) { async signUp(store, userInfo) {
const oauthStore = useOAuthStore() const oauthStore = useOAuthStore()
@ -624,7 +672,7 @@ const users = {
try { try {
const token = await oauthStore.ensureAppToken() const token = await oauthStore.ensureAppToken()
const data = await apiService.register({ const { data } = await register({
credentials: token, credentials: token,
params: { ...userInfo }, params: { ...userInfo },
}) })
@ -645,8 +693,10 @@ const users = {
throw e throw e
} }
}, },
async getCaptcha(store) { getCaptcha(store) {
return store.rootState.api.backendInteractor.getCaptcha() return getCaptcha({
credentials: useOAuthStore().token,
}).then(({ data }) => data)
}, },
logout(store) { logout(store) {
@ -663,24 +713,21 @@ const users = {
token: oauth.userToken, token: oauth.userToken,
} }
return oauthApi.revokeToken(params) return revokeToken(params)
}) })
.then(() => { .then(() => {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket') store.dispatch('disconnectFromSocket')
oauth.clearToken()
store.dispatch('stopFetchingTimeline', 'friends') store.dispatch('stopFetchingTimeline', 'friends')
store.commit(
'setBackendInteractor',
backendInteractorService(oauth.getToken),
)
store.dispatch('stopFetchingNotifications') store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingLists') useListsStore().stopFetching()
store.dispatch('stopFetchingBookmarkFolders') useBookmarkFoldersStore().stopFetching()
store.dispatch('stopFetchingFollowRequests') store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
store.dispatch('resetChats') store.dispatch('resetChats')
oauth.clearToken()
Cookies.remove('__Host-pleroma_key', { path: '/' })
useInterfaceStore().setLastTimeline('public-timeline') useInterfaceStore().setLastTimeline('public-timeline')
useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight()) useInterfaceStore().setLayoutHeight(windowHeight())
@ -690,141 +737,137 @@ const users = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const commit = store.commit const commit = store.commit
const dispatch = store.dispatch const dispatch = store.dispatch
commit('beginLogin') 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() verifyCredentials({
.initSyncConfig(user) credentials: useOAuthStore().token,
.then(() => { })
useInterfaceStore() .then(({ data: user }) => {
.applyTheme() // user.credentials = userCredentials
.catch((e) => { user.credentials = accessToken
console.error('Error setting theme', e) user.blockIds = []
}) user.muteIds = []
}) user.domainMutes = []
useUserHighlightStore().initUserHighlight(user) commit('setCurrentUser', user)
commit('addNewUsers', [user])
useEmojiStore().fetchEmoji() useSyncConfigStore()
.initSyncConfig(user)
getNotificationPermission().then((permission) => .then(() => {
useInterfaceStore().setNotificationPermission(permission), useInterfaceStore()
) .applyTheme()
.catch((e) => {
// Set our new backend interactor console.error('Error setting theme', e)
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 })
}) })
useUserHighlightStore().initUserHighlight(user)
commit('addNewUsers', [user])
// Reset flag to 0 to re-run migrations useEmojiStore().fetchEmoji()
useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 })
/**/
if (user.token) { getNotificationPermission().then((permission) =>
dispatch('setWsToken', user.token) useInterfaceStore().setNotificationPermission(permission),
)
// Initialize the shout socket. // Do server-side storage migrations
dispatch('initializeSocket')
}
const startPolling = () => { // Debug snippet to clean up storage and reset migrations
// Start getting fresh posts. /*
dispatch('startFetchingTimeline', { timeline: 'friends' }) // Reset wordfilter
Object.keys(
useSyncConfigStore().prefsStorage.simple.muteFilters
).forEach(key => {
useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null })
})
// Start fetching notifications // Reset flag to 0 to re-run migrations
dispatch('startFetchingNotifications') useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 })
/**/
if ( if (user.token) {
useInstanceCapabilitiesStore().pleromaChatMessagesAvailable dispatch('setWsToken', user.token)
) {
// Start fetching chats
dispatch('startFetchingChats')
}
}
dispatch('startFetchingLists') // Initialize the shout socket.
dispatch('startFetchingBookmarkFolders') dispatch('initializeSocket')
}
if (user.locked) { const startPolling = () => {
dispatch('startFetchingFollowRequests') // Start getting fresh posts.
} dispatch('startFetchingTimeline', { timeline: 'friends' })
if (useMergedConfigStore().mergedConfig.useStreamingApi) { // Start fetching notifications
dispatch('fetchTimeline', { timeline: 'friends', since: null }) dispatch('startFetchingNotifications')
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()
}
// Get user mutes if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
dispatch('fetchMutes') // Start fetching chats
dispatch('startFetchingChats')
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'))
} }
} }
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') commit('endLogin')
resolve() resolve()
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
// Authentication failed
commit('endLogin') 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)
}
}) })
}) })
}, },

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -13,8 +13,10 @@ function humanizeErrors(errors) {
export function StatusCodeError(statusCode, body, options, response) { export function StatusCodeError(statusCode, body, options, response) {
this.name = 'StatusCodeError' this.name = 'StatusCodeError'
this.statusCode = statusCode this.statusCode = statusCode
this.message = this.statusText = body.error || body.errors || body
statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : 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.error = body // legacy attribute
this.options = options this.options = options
this.response = response this.response = response

View file

@ -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) => const fetchRelationship = (attempt, userId, store) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
store.state.api.backendInteractor fetchUserRelationship({
.fetchUserRelationship({ id: userId }) id: userId,
.then((relationship) => { credentials: useOAuthStore().token,
})
.then(({ data: relationship }) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
return relationship return relationship
}) })
@ -25,40 +35,33 @@ const fetchRelationship = (attempt, userId, store) =>
} }
}) })
export const requestFollow = (userId, store) => export const requestFollow = async (userId, store) => {
new Promise((resolve) => { const { data: updated } = await followUser({
store.state.api.backendInteractor id: userId,
.followUser({ id: userId }) credentials: useOAuthStore().token,
.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 requestUnfollow = (userId, store) => store.commit('updateUserRelationship', [updated])
new Promise((resolve) => {
store.state.api.backendInteractor if (updated.following || (updated.locked && updated.requested)) {
.unfollowUser({ id: userId }) // If we get result immediately or the account is locked, just stop.
.then((updated) => { return
store.commit('updateUserRelationship', [updated]) }
resolve({
updated, // 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])
}

View file

@ -1,11 +1,10 @@
import apiService from '../api/api.service.js' import { fetchFollowRequests } from 'src/api/user.js'
import { promiseInterval } from '../promise_interval/promise_interval.js' import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
const fetchAndUpdate = ({ store, credentials }) => { const fetchAndUpdate = ({ store, credentials }) => {
return apiService return fetchFollowRequests({ credentials })
.fetchFollowRequests({ credentials })
.then( .then(
(requests) => { ({ data: requests }) => {
store.commit('setFollowRequests', requests) store.commit('setFollowRequests', requests)
store.commit('addNewUsers', requests) store.commit('addNewUsers', requests)
}, },

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,8 +1,6 @@
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
import { muteFilterHits } from '../status_parser/status_parser.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' import FaviconService from 'src/services/favicon_service/favicon_service.js'
export const ACTIONABLE_NOTIFICATION_TYPES = new Set([ export const ACTIONABLE_NOTIFICATION_TYPES = new Set([

View file

@ -1,11 +1,11 @@
import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { fetchTimeline } from 'src/api/timelines.js'
const update = ({ store, notifications, older }) => { const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older }) store.dispatch('addNewNotifications', { notifications, older })
} }
@ -25,7 +25,7 @@ const mastoApiNotificationTypes = new Set([
'pleroma:report', 'pleroma:report',
]) ])
const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => {
const args = { credentials } const args = { credentials }
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.notifications const timelineData = rootState.notifications
@ -35,24 +35,24 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
mastoApiNotificationTypes.add('pleroma:chat_mention') mastoApiNotificationTypes.add('pleroma:chat_mention')
} }
args.includeTypes = mastoApiNotificationTypes args.includeTypes = [...mastoApiNotificationTypes]
args.withMuted = !hideMutedPosts args.withMuted = !hideMutedPosts
args.timeline = 'notifications' args.timeline = 'notifications'
if (older) { if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) { if (timelineData.minId !== Number.POSITIVE_INFINITY) {
args.until = timelineData.minId args.maxId = timelineData.minId
} }
return fetchNotifications({ store, args, older }) return fetchNotifications({ store, args, older })
} else { } else {
// fetch new notifications // fetch new notifications
if ( if (
since === undefined && sinceId === undefined &&
timelineData.maxId !== Number.POSITIVE_INFINITY timelineData.maxId !== Number.POSITIVE_INFINITY
) { ) {
args.since = timelineData.maxId args.sinceId = timelineData.maxId
} else if (since !== null) { } else if (sinceId !== null) {
args.since = since args.sinceId = sinceId
} }
const result = fetchNotifications({ store, args, older }) const result = fetchNotifications({ store, args, older })
@ -69,7 +69,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
if (readNotifsIds.length > 0 && readNotifsIds.length > 0) { if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
if (minId !== Infinity) { 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 args.minId = minId - 1 // go beyond
fetchNotifications({ store, args, older }) fetchNotifications({ store, args, older })
} }
@ -80,29 +80,25 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
} }
const fetchNotifications = ({ store, args, older }) => { const fetchNotifications = ({ store, args, older }) => {
return apiService return fetchTimeline(args)
.fetchTimeline(args)
.then((response) => { .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 const notifications = response.data
update({ store, notifications, older }) update({ store, notifications, older })
return notifications return notifications
}) })
.catch((error) => { .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({ useInterfaceStore().pushGlobalNotice({
level: 'error', level: 'error',
messageKey: 'notifications.error', messageKey: 'notifications.error',

View file

@ -4,32 +4,34 @@
// time after the first interval. // time after the first interval.
// - interval is the interval delay in ms. // - 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) => { export const promiseInterval = (promiseCall, interval) => {
let stopped = false let stopped = false
let timeout = null 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 = () => { const stopFetcher = () => {
stopped = true stopped = true
window.clearTimeout(timeout) 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 } return { stop: stopFetcher }
} }

Some files were not shown because too many files have changed in this diff Show more