aboutsummaryrefslogtreecommitdiff
path: root/src/services/api/api.service.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/services/api/api.service.js')
-rw-r--r--src/services/api/api.service.js286
1 files changed, 243 insertions, 43 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 8f5eb416..dfffc291 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,10 +1,8 @@
-import { each, map, concat, last } from 'lodash'
+import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
-import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
-const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -12,22 +10,25 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
-const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenName}/activation_status`
+const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
+const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
-const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp'
+const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@@ -71,6 +72,12 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
+const MASTODON_STREAMING = '/api/v1/streaming'
+const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
+const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
+const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const oldfetch = window.fetch
@@ -317,7 +324,8 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
- limit && `limit=${limit}`
+ limit && `limit=${limit}`,
+ `with_relationships=true`
].filter(_ => _).join('&')
url = url + (args ? '?' + args : '')
@@ -351,7 +359,8 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
- limit && `limit=${limit}`
+ limit && `limit=${limit}`,
+ `with_relationships=true`
].filter(_ => _).join('&')
url += args ? '?' + args : ''
@@ -396,8 +405,8 @@ const fetchStatus = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
-const tagUser = ({ tag, credentials, ...options }) => {
- const screenName = options.screen_name
+const tagUser = ({ tag, credentials, user }) => {
+ const screenName = user.screen_name
const form = {
nicknames: [screenName],
tags: [tag]
@@ -413,8 +422,8 @@ const tagUser = ({ tag, credentials, ...options }) => {
})
}
-const untagUser = ({ tag, credentials, ...options }) => {
- const screenName = options.screen_name
+const untagUser = ({ tag, credentials, user }) => {
+ const screenName = user.screen_name
const body = {
nicknames: [screenName],
tags: [tag]
@@ -430,7 +439,7 @@ const untagUser = ({ tag, credentials, ...options }) => {
})
}
-const addRight = ({ right, credentials, ...user }) => {
+const addRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@@ -440,7 +449,7 @@ const addRight = ({ right, credentials, ...user }) => {
})
}
-const deleteRight = ({ right, credentials, ...user }) => {
+const deleteRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
@@ -450,23 +459,29 @@ const deleteRight = ({ right, credentials, ...user }) => {
})
}
-const setActivationStatus = ({ status, credentials, ...user }) => {
- const screenName = user.screen_name
- const body = {
- status: status
- }
-
- const headers = authHeaders(credentials)
- headers['Content-Type'] = 'application/json'
+const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
+ return promisedRequest({
+ url: ACTIVATE_USER_URL,
+ method: 'PATCH',
+ credentials,
+ payload: {
+ nicknames: [nickname]
+ }
+ }).then(response => get(response, 'users.0'))
+}
- return fetch(ACTIVATION_STATUS_URL(screenName), {
- method: 'PUT',
- headers: headers,
- body: JSON.stringify(body)
- })
+const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
+ return promisedRequest({
+ url: DEACTIVATE_USER_URL,
+ method: 'PATCH',
+ credentials,
+ payload: {
+ nicknames: [nickname]
+ }
+ }).then(response => get(response, 'users.0'))
}
-const deleteUser = ({ credentials, ...user }) => {
+const deleteUser = ({ credentials, user }) => {
const screenName = user.screen_name
const headers = authHeaders(credentials)
@@ -523,22 +538,32 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
+ if (timeline !== 'favorites') {
+ params.push(['with_muted', withMuted])
+ }
- params.push(['count', 20])
- params.push(['with_muted', withMuted])
+ params.push(['limit', 20])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
-
+ let status = ''
+ let statusText = ''
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
- if (data.ok) {
+ status = data.status
+ statusText = data.statusText
+ return data
+ })
+ .then((data) => data.json())
+ .then((data) => {
+ if (!data.error) {
+ return data.map(isNotifications ? parseNotification : parseStatus)
+ } else {
+ data.status = status
+ data.statusText = statusText
return data
}
- throw new Error('Error fetching timeline', data)
})
- .then((data) => data.json())
- .then((data) => data.map(isNotifications ? parseNotification : parseStatus))
}
const fetchPinnedStatuses = ({ id, credentials }) => {
@@ -820,12 +845,16 @@ const suggestions = ({ credentials }) => {
}).then((data) => data.json())
}
-const markNotificationsAsSeen = ({ id, credentials }) => {
+const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
const body = new FormData()
- body.append('latest_id', id)
+ if (single) {
+ body.append('id', id)
+ } else {
+ body.append('max_id', id)
+ }
- return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
+ return fetch(NOTIFICATION_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
@@ -856,12 +885,44 @@ const fetchPoll = ({ pollId, credentials }) => {
)
}
-const fetchFavoritedByUsers = ({ id }) => {
- return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
+const fetchFavoritedByUsers = ({ id, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_STATUS_FAVORITEDBY_URL(id),
+ method: 'GET',
+ credentials
+ }).then((users) => users.map(parseUser))
+}
+
+const fetchRebloggedByUsers = ({ id, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
+ method: 'GET',
+ credentials
+ }).then((users) => users.map(parseUser))
}
-const fetchRebloggedByUsers = ({ id }) => {
- return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
+const fetchEmojiReactions = ({ id, credentials }) => {
+ return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
+ .then((reactions) => reactions.map(r => {
+ r.accounts = r.accounts.map(parseUser)
+ return r
+ }))
+}
+
+const reactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_REACT_URL(id, emoji),
+ method: 'PUT',
+ credentials
+ }).then(parseStatus)
+}
+
+const unreactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
+ method: 'DELETE',
+ credentials
+ }).then(parseStatus)
}
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
@@ -914,6 +975,8 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
params.push(['following', true])
}
+ params.push(['with_relationships', true])
+
let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
@@ -932,6 +995,134 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+const fetchKnownDomains = ({ credentials }) => {
+ return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
+}
+
+const fetchDomainMutes = ({ credentials }) => {
+ return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
+}
+
+const muteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'POST',
+ payload: { domain },
+ credentials
+ })
+}
+
+const unmuteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'DELETE',
+ payload: { domain },
+ credentials
+ })
+}
+
+const dismissNotification = ({ credentials, id }) => {
+ return promisedRequest({
+ url: MASTODON_DISMISS_NOTIFICATION_URL(id),
+ method: 'POST',
+ payload: { id },
+ credentials
+ })
+}
+
+export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
+ return Object.entries({
+ ...(credentials
+ ? { access_token: credentials }
+ : {}
+ ),
+ stream,
+ ...args
+ }).reduce((acc, [key, val]) => {
+ return acc + `${key}=${val}&`
+ }, MASTODON_STREAMING + '?')
+}
+
+const MASTODON_STREAMING_EVENTS = new Set([
+ 'update',
+ 'notification',
+ 'delete',
+ 'filters_changed'
+])
+
+// 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'
+}) => {
+ 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)
+ })
+ 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
+ )
+ })
+ /**/
+
+ proxy(socket, 'open')
+ proxy(socket, 'close')
+ proxy(socket, 'message', preprocessor)
+ proxy(socket, 'error')
+
+ // 1000 = Normal Closure
+ eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
+
+ return eventTarget
+}
+
+export const handleMastoWS = (wsEvent) => {
+ const { data } = wsEvent
+ if (!data) return
+ const parsedEvent = JSON.parse(data)
+ const { event, payload } = parsedEvent
+ if (MASTODON_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 === 'update') {
+ return { event, status: parseStatus(data) }
+ } else if (event === 'notification') {
+ return { event, notification: parseNotification(data) }
+ }
+ } else {
+ console.warn('Unknown event', wsEvent)
+ return null
+ }
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -971,7 +1162,8 @@ const apiService = {
deleteUser,
addRight,
deleteRight,
- setActivationStatus,
+ activateUser,
+ deactivateUser,
register,
getCaptcha,
updateAvatar,
@@ -993,14 +1185,22 @@ const apiService = {
denyUser,
suggestions,
markNotificationsAsSeen,
+ dismissNotification,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
+ fetchEmojiReactions,
+ reactWithEmoji,
+ unreactWithEmoji,
reportUser,
updateNotificationSettings,
search2,
- searchUsers
+ searchUsers,
+ fetchKnownDomains,
+ fetchDomainMutes,
+ muteDomain,
+ unmuteDomain
}
export default apiService