diff options
Diffstat (limited to 'src/services')
23 files changed, 812 insertions, 338 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 162b62f7..61cd4f16 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,24 +1,30 @@ +import { each, map, concat, last } 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 LOGIN_URL = '/api/account/verify_credentials.json' -const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' -const MENTIONS_URL = '/api/statuses/mentions.json' -const REGISTRATION_URL = '/api/account/register.json' -const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' -const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' 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' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' -const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests' -const APPROVE_USER_URL = '/api/pleroma/friendships/approve' -const DENY_USER_URL = '/api/pleroma/friendships/deny' 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 ADMIN_USERS_URL = '/api/pleroma/admin/users' const SUGGESTIONS_URL = '/api/v1/suggestions' +const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' + +const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa' +const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes' + +const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp' +const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp' +const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/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_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` @@ -30,6 +36,9 @@ const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow` const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow` const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following` const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers` +const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' +const MASTODON_APPROVE_USER_URL = id => `/api/v1/follow_requests/${id}/authorize` +const MASTODON_DENY_USER_URL = id => `/api/v1/follow_requests/${id}/reject` const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct' const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' @@ -45,19 +54,22 @@ 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_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe` +const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe` const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' +const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes` +const MASTODON_POLL_URL = id => `/api/v1/polls/${id}` const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by` const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by` const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' const MASTODON_REPORT_USER_URL = '/api/v1/reports' const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin` const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin` - -import { each, map, concat, last } from 'lodash' -import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' -import 'whatwg-fetch' -import { StatusCodeError } from '../errors/errors' +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 oldfetch = window.fetch @@ -69,7 +81,7 @@ let fetch = (url, options) => { return oldfetch(fullUrl, options) } -const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => { +const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => { const options = { method, headers: { @@ -78,6 +90,11 @@ const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => ...headers } } + if (params) { + url += '?' + Object.entries(params) + .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) + .join('&') + } if (payload) { options.body = JSON.stringify(payload) } @@ -99,56 +116,61 @@ const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => }) } -const updateAvatar = ({credentials, avatar}) => { +const updateNotificationSettings = ({ credentials, settings }) => { + const form = new FormData() + + each(settings, (value, key) => { + form.append(key, value) + }) + + return fetch(NOTIFICATION_SETTINGS_URL, { + headers: authHeaders(credentials), + method: 'PUT', + body: form + }).then((data) => data.json()) +} + +const updateAvatar = ({ credentials, avatar }) => { const form = new FormData() form.append('avatar', avatar) return fetch(MASTODON_PROFILE_UPDATE_URL, { headers: authHeaders(credentials), method: 'PATCH', body: form - }) - .then((data) => data.json()) - .then((data) => parseUser(data)) + }).then((data) => data.json()) + .then((data) => parseUser(data)) } -const updateBg = ({credentials, params}) => { - let url = BG_UPDATE_URL - +const updateBg = ({ credentials, background }) => { const form = new FormData() - - each(params, (value, key) => { - if (value) { - form.append(key, value) - } - }) - - return fetch(url, { + form.append('pleroma_background_image', background) + return fetch(MASTODON_PROFILE_UPDATE_URL, { headers: authHeaders(credentials), - method: 'POST', + method: 'PATCH', body: form - }).then((data) => data.json()) + }) + .then((data) => data.json()) + .then((data) => parseUser(data)) } -const updateBanner = ({credentials, banner}) => { +const updateBanner = ({ credentials, banner }) => { const form = new FormData() form.append('header', banner) return fetch(MASTODON_PROFILE_UPDATE_URL, { headers: authHeaders(credentials), method: 'PATCH', body: form - }) - .then((data) => data.json()) - .then((data) => parseUser(data)) + }).then((data) => data.json()) + .then((data) => parseUser(data)) } -const updateProfile = ({credentials, params}) => { +const updateProfile = ({ credentials, params }) => { return promisedRequest({ url: MASTODON_PROFILE_UPDATE_URL, method: 'PATCH', payload: params, credentials - }) - .then((data) => parseUser(data)) + }).then((data) => parseUser(data)) } // Params needed: @@ -163,19 +185,28 @@ const updateProfile = ({credentials, params}) => { // homepage // location // token -const register = (params) => { - const form = new FormData() - - each(params, (value, key) => { - if (value) { - form.append(key, value) - } - }) - - return fetch(REGISTRATION_URL, { +const register = ({ params, credentials }) => { + const { nickname, ...rest } = params + return fetch(MASTODON_REGISTRATION_URL, { method: 'POST', - body: form + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nickname, + locale: 'en_US', + agreement: true, + ...rest + }) }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return response.json().then((error) => { throw new RegistrationError(error) }) + } + }) } const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json()) @@ -188,23 +219,21 @@ const authHeaders = (accessToken) => { } } -const externalProfile = ({profileUrl, credentials}) => { - let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}` - return fetch(url, { - headers: authHeaders(credentials), - method: 'GET' - }).then((data) => data.json()) -} - -const followUser = ({id, credentials}) => { +const followUser = ({ id, credentials, ...options }) => { let url = MASTODON_FOLLOW_URL(id) + const form = {} + if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs } return fetch(url, { - headers: authHeaders(credentials), + body: JSON.stringify(form), + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, method: 'POST' }).then((data) => data.json()) } -const unfollowUser = ({id, credentials}) => { +const unfollowUser = ({ id, credentials }) => { let url = MASTODON_UNFOLLOW_URL(id) return fetch(url, { headers: authHeaders(credentials), @@ -222,43 +251,53 @@ const unpinOwnStatus = ({ id, credentials }) => { .then((data) => parseStatus(data)) } -const blockUser = ({id, credentials}) => { +const muteConversation = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_MUTE_CONVERSATION(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + +const unmuteConversation = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_UNMUTE_CONVERSATION(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + +const blockUser = ({ id, credentials }) => { return fetch(MASTODON_BLOCK_USER_URL(id), { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) } -const unblockUser = ({id, credentials}) => { +const unblockUser = ({ id, credentials }) => { return fetch(MASTODON_UNBLOCK_USER_URL(id), { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) } -const approveUser = ({id, credentials}) => { - let url = `${APPROVE_USER_URL}?user_id=${id}` +const approveUser = ({ id, credentials }) => { + let url = MASTODON_APPROVE_USER_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) } -const denyUser = ({id, credentials}) => { - let url = `${DENY_USER_URL}?user_id=${id}` +const denyUser = ({ id, credentials }) => { + let url = MASTODON_DENY_USER_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) } -const fetchUser = ({id, credentials}) => { +const fetchUser = ({ id, credentials }) => { let url = `${MASTODON_USER_URL}/${id}` return promisedRequest({ url, credentials }) .then((data) => parseUser(data)) } -const fetchUserRelationship = ({id, credentials}) => { +const fetchUserRelationship = ({ id, credentials }) => { let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((response) => { @@ -272,7 +311,7 @@ const fetchUserRelationship = ({id, credentials}) => { }) } -const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => { +const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => { let url = MASTODON_FOLLOWING_URL(id) const args = [ maxId && `max_id=${maxId}`, @@ -286,14 +325,14 @@ const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => { .then((data) => data.map(parseUser)) } -const exportFriends = ({id, credentials}) => { +const exportFriends = ({ id, credentials }) => { 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}) + const users = await fetchFriends({ id, maxId, credentials }) friends = concat(friends, users) if (users.length === 0) { more = false @@ -306,7 +345,7 @@ const exportFriends = ({id, credentials}) => { }) } -const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => { +const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => { let url = MASTODON_FOLLOWERS_URL(id) const args = [ maxId && `max_id=${maxId}`, @@ -320,20 +359,14 @@ const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => { .then((data) => data.map(parseUser)) } -const fetchAllFollowing = ({username, credentials}) => { - const url = `${ALL_FOLLOWING_URL}/${username}.json` +const fetchFollowRequests = ({ credentials }) => { + const url = MASTODON_FOLLOW_REQUESTS_URL return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) } -const fetchFollowRequests = ({credentials}) => { - const url = FOLLOW_REQUESTS_URL - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) -} - -const fetchConversation = ({id, credentials}) => { +const fetchConversation = ({ id, credentials }) => { let urlContext = MASTODON_STATUS_CONTEXT_URL(id) return fetch(urlContext, { headers: authHeaders(credentials) }) .then((data) => { @@ -343,13 +376,13 @@ const fetchConversation = ({id, credentials}) => { throw new Error('Error fetching timeline', data) }) .then((data) => data.json()) - .then(({ancestors, descendants}) => ({ + .then(({ ancestors, descendants }) => ({ ancestors: ancestors.map(parseStatus), descendants: descendants.map(parseStatus) })) } -const fetchStatus = ({id, credentials}) => { +const fetchStatus = ({ id, credentials }) => { let url = MASTODON_STATUS_URL(id) return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { @@ -362,7 +395,7 @@ const fetchStatus = ({id, credentials}) => { .then((data) => parseStatus(data)) } -const tagUser = ({tag, credentials, ...options}) => { +const tagUser = ({ tag, credentials, ...options }) => { const screenName = options.screen_name const form = { nicknames: [screenName], @@ -379,7 +412,7 @@ const tagUser = ({tag, credentials, ...options}) => { }) } -const untagUser = ({tag, credentials, ...options}) => { +const untagUser = ({ tag, credentials, ...options }) => { const screenName = options.screen_name const body = { nicknames: [screenName], @@ -396,7 +429,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), { @@ -406,7 +439,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), { @@ -416,7 +449,7 @@ const deleteRight = ({right, credentials, ...user}) => { }) } -const setActivationStatus = ({status, credentials, ...user}) => { +const setActivationStatus = ({ status, credentials, ...user }) => { const screenName = user.screen_name const body = { status: status @@ -432,7 +465,7 @@ const setActivationStatus = ({status, credentials, ...user}) => { }) } -const deleteUser = ({credentials, ...user}) => { +const deleteUser = ({ credentials, ...user }) => { const screenName = user.screen_name const headers = authHeaders(credentials) @@ -442,11 +475,18 @@ const deleteUser = ({credentials, ...user}) => { }) } -const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { +const fetchTimeline = ({ + timeline, + credentials, + since = false, + until = false, + userId = false, + tag = false, + withMuted = false +}) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, friends: MASTODON_USER_HOME_TIMELINE_URL, - mentions: MENTIONS_URL, dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, notifications: MASTODON_USER_NOTIFICATIONS_URL, 'publicAndExternal': MASTODON_PUBLIC_TIMELINE, @@ -507,8 +547,7 @@ const fetchPinnedStatuses = ({ id, credentials }) => { } const verifyCredentials = (user) => { - return fetch(LOGIN_URL, { - method: 'POST', + return fetch(MASTODON_LOGIN_URL, { headers: authHeaders(user) }) .then((response) => { @@ -543,8 +582,19 @@ const unretweet = ({ id, credentials }) => { .then((data) => parseStatus(data)) } -const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => { +const postStatus = ({ + credentials, + status, + spoilerText, + visibility, + sensitive, + poll, + mediaIds = [], + inReplyToStatusId, + contentType +}) => { const form = new FormData() + const pollOptions = poll.options || [] form.append('status', status) form.append('source', 'Pleroma FE') @@ -555,6 +605,19 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me mediaIds.forEach(val => { form.append('media_ids[]', val) }) + if (pollOptions.some(option => option !== '')) { + const normalizedPoll = { + expires_in: poll.expiresIn, + 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) } @@ -583,7 +646,7 @@ const deleteStatus = ({ id, credentials }) => { }) } -const uploadMedia = ({formData, credentials}) => { +const uploadMedia = ({ formData, credentials }) => { return fetch(MASTODON_MEDIA_UPLOAD_URL, { body: formData, method: 'POST', @@ -593,7 +656,7 @@ const uploadMedia = ({formData, credentials}) => { .then((data) => parseAttachment(data)) } -const importBlocks = ({file, credentials}) => { +const importBlocks = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) return fetch(BLOCKS_IMPORT_URL, { @@ -604,7 +667,7 @@ const importBlocks = ({file, credentials}) => { .then((response) => response.ok) } -const importFollows = ({file, credentials}) => { +const importFollows = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) return fetch(FOLLOW_IMPORT_URL, { @@ -615,7 +678,7 @@ const importFollows = ({file, credentials}) => { .then((response) => response.ok) } -const deleteAccount = ({credentials, password}) => { +const deleteAccount = ({ credentials, password }) => { const form = new FormData() form.append('password', password) @@ -628,7 +691,7 @@ const deleteAccount = ({credentials, password}) => { .then((response) => response.json()) } -const changePassword = ({credentials, password, newPassword, newPasswordConfirmation}) => { +const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => { const form = new FormData() form.append('password', password) @@ -643,25 +706,78 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma .then((response) => response.json()) } -const fetchMutes = ({credentials}) => { +const settingsMFA = ({ credentials }) => { + return fetch(MFA_SETTINGS_URL, { + headers: authHeaders(credentials), + method: 'GET' + }).then((data) => data.json()) +} + +const mfaDisableOTP = ({ credentials, password }) => { + const form = new FormData() + + form.append('password', password) + + return fetch(MFA_DISABLE_OTP_URL, { + body: form, + method: 'DELETE', + headers: authHeaders(credentials) + }) + .then((response) => response.json()) +} + +const mfaConfirmOTP = ({ credentials, password, token }) => { + const form = new FormData() + + form.append('password', password) + form.append('code', token) + + return fetch(MFA_CONFIRM_OTP_URL, { + body: form, + headers: authHeaders(credentials), + method: 'POST' + }).then((data) => data.json()) +} +const mfaSetupOTP = ({ credentials }) => { + return fetch(MFA_SETUP_OTP_URL, { + headers: authHeaders(credentials), + method: 'GET' + }).then((data) => data.json()) +} +const generateMfaBackupCodes = ({ credentials }) => { + return fetch(MFA_BACKUP_CODES_URL, { + headers: authHeaders(credentials), + method: 'GET' + }).then((data) => data.json()) +} + +const fetchMutes = ({ credentials }) => { return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials }) .then((users) => users.map(parseUser)) } -const muteUser = ({id, credentials}) => { +const muteUser = ({ id, credentials }) => { return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' }) } -const unmuteUser = ({id, credentials}) => { +const unmuteUser = ({ id, credentials }) => { return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' }) } -const fetchBlocks = ({credentials}) => { +const subscribeUser = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_SUBSCRIBE_USER(id), credentials, method: 'POST' }) +} + +const unsubscribeUser = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_UNSUBSCRIBE_USER(id), credentials, method: 'POST' }) +} + +const fetchBlocks = ({ credentials }) => { return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials }) .then((users) => users.map(parseUser)) } -const fetchOAuthTokens = ({credentials}) => { +const fetchOAuthTokens = ({ credentials }) => { const url = '/api/oauth_tokens.json' return fetch(url, { @@ -674,7 +790,7 @@ const fetchOAuthTokens = ({credentials}) => { }) } -const revokeOAuthToken = ({id, credentials}) => { +const revokeOAuthToken = ({ id, credentials }) => { const url = `/api/oauth_tokens/${id}` return fetch(url, { @@ -683,13 +799,13 @@ const revokeOAuthToken = ({id, credentials}) => { }) } -const suggestions = ({credentials}) => { +const suggestions = ({ credentials }) => { return fetch(SUGGESTIONS_URL, { headers: authHeaders(credentials) }).then((data) => data.json()) } -const markNotificationsAsSeen = ({id, credentials}) => { +const markNotificationsAsSeen = ({ id, credentials }) => { const body = new FormData() body.append('latest_id', id) @@ -701,15 +817,39 @@ const markNotificationsAsSeen = ({id, credentials}) => { }).then((data) => data.json()) } -const fetchFavoritedByUsers = ({id}) => { +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: choices + } + }) +} + +const fetchPoll = ({ pollId, credentials }) => { + return promisedRequest( + { + url: MASTODON_POLL_URL(encodeURIComponent(pollId)), + method: 'GET', + credentials + } + ) +} + +const fetchFavoritedByUsers = ({ id }) => { return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser)) } -const fetchRebloggedByUsers = ({id}) => { +const fetchRebloggedByUsers = ({ id }) => { return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser)) } -const reportUser = ({credentials, userId, statusIds, comment, forward}) => { +const reportUser = ({ credentials, userId, statusIds, comment, forward }) => { return promisedRequest({ url: MASTODON_REPORT_USER_URL, method: 'POST', @@ -723,6 +863,60 @@ const reportUser = ({credentials, userId, statusIds, comment, forward}) => { }) } +const searchUsers = ({ credentials, query }) => { + return promisedRequest({ + url: MASTODON_USER_SEARCH_URL, + params: { + q: query, + resolve: true + }, + credentials + }) + .then((data) => data.map(parseUser)) +} + +const search2 = ({ credentials, q, resolve, limit, offset, following }) => { + let url = MASTODON_SEARCH_2 + let params = [] + + if (q) { + params.push(['q', encodeURIComponent(q)]) + } + + if (resolve) { + params.push(['resolve', resolve]) + } + + if (limit) { + params.push(['limit', limit]) + } + + if (offset) { + params.push(['offset', offset]) + } + + if (following) { + params.push(['following', true]) + } + + let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') + url += `?${queryString}` + + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => { + if (data.ok) { + return data + } + throw new Error('Error fetching search result', data) + }) + .then((data) => { return data.json() }) + .then((data) => { + data.accounts = data.accounts.slice(0, limit).map(u => parseUser(u)) + data.statuses = data.statuses.slice(0, limit).map(s => parseStatus(s)) + return data + }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -736,6 +930,8 @@ const apiService = { unfollowUser, pinOwnStatus, unpinOwnStatus, + muteConversation, + unmuteConversation, blockUser, unblockUser, fetchUser, @@ -747,10 +943,11 @@ const apiService = { postStatus, deleteStatus, uploadMedia, - fetchAllFollowing, fetchMutes, muteUser, unmuteUser, + subscribeUser, + unsubscribeUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, @@ -766,19 +963,28 @@ const apiService = { updateBg, updateProfile, updateBanner, - externalProfile, importBlocks, importFollows, deleteAccount, changePassword, + settingsMFA, + mfaDisableOTP, + generateMfaBackupCodes, + mfaSetupOTP, + mfaConfirmOTP, fetchFollowRequests, approveUser, denyUser, suggestions, markNotificationsAsSeen, + vote, + fetchPoll, fetchFavoritedByUsers, fetchRebloggedByUsers, - reportUser + reportUser, + updateNotificationSettings, + search2, + searchUsers } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index e23e1222..cbf48ee4 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -2,61 +2,57 @@ import apiService from '../api/api.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' -const backendInteractorService = (credentials) => { - const fetchStatus = ({id}) => { - return apiService.fetchStatus({id, credentials}) +const backendInteractorService = credentials => { + const fetchStatus = ({ id }) => { + return apiService.fetchStatus({ id, credentials }) } - const fetchConversation = ({id}) => { - return apiService.fetchConversation({id, credentials}) + const fetchConversation = ({ id }) => { + return apiService.fetchConversation({ id, credentials }) } - const fetchFriends = ({id, maxId, sinceId, limit}) => { - return apiService.fetchFriends({id, maxId, sinceId, limit, credentials}) + const fetchFriends = ({ id, maxId, sinceId, limit }) => { + return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials }) } - const exportFriends = ({id}) => { - return apiService.exportFriends({id, credentials}) + const exportFriends = ({ id }) => { + return apiService.exportFriends({ id, credentials }) } - const fetchFollowers = ({id, maxId, sinceId, limit}) => { - return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials}) + const fetchFollowers = ({ id, maxId, sinceId, limit }) => { + return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials }) } - const fetchAllFollowing = ({username}) => { - return apiService.fetchAllFollowing({username, credentials}) + const fetchUser = ({ id }) => { + return apiService.fetchUser({ id, credentials }) } - const fetchUser = ({id}) => { - return apiService.fetchUser({id, credentials}) + const fetchUserRelationship = ({ id }) => { + return apiService.fetchUserRelationship({ id, credentials }) } - const fetchUserRelationship = ({id}) => { - return apiService.fetchUserRelationship({id, credentials}) - } - - const followUser = (id) => { - return apiService.followUser({credentials, id}) + const followUser = ({ id, reblogs }) => { + return apiService.followUser({ credentials, id, reblogs }) } const unfollowUser = (id) => { - return apiService.unfollowUser({credentials, id}) + return apiService.unfollowUser({ credentials, id }) } const blockUser = (id) => { - return apiService.blockUser({credentials, id}) + return apiService.blockUser({ credentials, id }) } const unblockUser = (id) => { - return apiService.unblockUser({credentials, id}) + return apiService.unblockUser({ credentials, id }) } const approveUser = (id) => { - return apiService.approveUser({credentials, id}) + return apiService.approveUser({ credentials, id }) } const denyUser = (id) => { - return apiService.denyUser({credentials, id}) + return apiService.denyUser({ credentials, id }) } const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => { @@ -67,63 +63,94 @@ const backendInteractorService = (credentials) => { return notificationsFetcher.startFetching({ store, credentials }) } - const tagUser = ({screen_name}, tag) => { - return apiService.tagUser({screen_name, tag, credentials}) + // eslint-disable-next-line camelcase + const tagUser = ({ screen_name }, tag) => { + return apiService.tagUser({ screen_name, tag, credentials }) } - const untagUser = ({screen_name}, tag) => { - return apiService.untagUser({screen_name, tag, credentials}) + // eslint-disable-next-line camelcase + const untagUser = ({ screen_name }, tag) => { + return apiService.untagUser({ screen_name, tag, credentials }) } - const addRight = ({screen_name}, right) => { - return apiService.addRight({screen_name, right, credentials}) + // eslint-disable-next-line camelcase + const addRight = ({ screen_name }, right) => { + return apiService.addRight({ screen_name, right, credentials }) } - const deleteRight = ({screen_name}, right) => { - return apiService.deleteRight({screen_name, right, credentials}) + // eslint-disable-next-line camelcase + const deleteRight = ({ screen_name }, right) => { + return apiService.deleteRight({ screen_name, right, credentials }) } - const setActivationStatus = ({screen_name}, status) => { - return apiService.setActivationStatus({screen_name, status, credentials}) + // eslint-disable-next-line camelcase + const setActivationStatus = ({ screen_name }, status) => { + return apiService.setActivationStatus({ screen_name, status, credentials }) } - const deleteUser = ({screen_name}) => { - return apiService.deleteUser({screen_name, credentials}) + // eslint-disable-next-line camelcase + const deleteUser = ({ screen_name }) => { + return apiService.deleteUser({ screen_name, credentials }) } - const fetchMutes = () => apiService.fetchMutes({credentials}) - const muteUser = (id) => apiService.muteUser({credentials, id}) - const unmuteUser = (id) => apiService.unmuteUser({credentials, id}) - const fetchBlocks = () => apiService.fetchBlocks({credentials}) - const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) - const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials}) - const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials}) - const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({credentials, id}) - const pinOwnStatus = (id) => apiService.pinOwnStatus({credentials, id}) - const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id}) - - const getCaptcha = () => apiService.getCaptcha() - const register = (params) => apiService.register(params) - const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar}) - const updateBg = ({params}) => apiService.updateBg({credentials, params}) - const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner}) - const updateProfile = ({params}) => apiService.updateProfile({credentials, params}) + const vote = (pollId, choices) => { + return apiService.vote({ credentials, pollId, choices }) + } - const externalProfile = (profileUrl) => apiService.externalProfile({profileUrl, credentials}) - const importBlocks = (file) => apiService.importBlocks({file, credentials}) - const importFollows = (file) => apiService.importFollows({file, credentials}) + const fetchPoll = (pollId) => { + return apiService.fetchPoll({ credentials, pollId }) + } - const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password}) - const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation}) + const updateNotificationSettings = ({ settings }) => { + return apiService.updateNotificationSettings({ credentials, settings }) + } - const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id}) - const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id}) - const reportUser = (params) => apiService.reportUser({credentials, ...params}) + const fetchMutes = () => apiService.fetchMutes({ credentials }) + const muteUser = (id) => apiService.muteUser({ credentials, id }) + const unmuteUser = (id) => apiService.unmuteUser({ credentials, id }) + const subscribeUser = (id) => apiService.subscribeUser({ credentials, id }) + const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id }) + const fetchBlocks = () => apiService.fetchBlocks({ credentials }) + const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials }) + const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials }) + const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials }) + const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id }) + const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id }) + const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id }) + const muteConversation = (id) => apiService.muteConversation({ credentials, id }) + const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id }) - const favorite = (id) => apiService.favorite({id, credentials}) - const unfavorite = (id) => apiService.unfavorite({id, credentials}) - const retweet = (id) => apiService.retweet({id, credentials}) - const unretweet = (id) => apiService.unretweet({id, credentials}) + const getCaptcha = () => apiService.getCaptcha() + const register = (params) => apiService.register({ credentials, params }) + const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar }) + const updateBg = ({ background }) => apiService.updateBg({ credentials, background }) + const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner }) + const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params }) + + const importBlocks = (file) => apiService.importBlocks({ file, credentials }) + const importFollows = (file) => apiService.importFollows({ file, credentials }) + + const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password }) + const changePassword = ({ password, newPassword, newPasswordConfirmation }) => + apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation }) + + const fetchSettingsMFA = () => apiService.settingsMFA({ credentials }) + const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials }) + const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials }) + const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token }) + const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password }) + + const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id }) + const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id }) + const reportUser = (params) => apiService.reportUser({ credentials, ...params }) + + const favorite = (id) => apiService.favorite({ id, credentials }) + const unfavorite = (id) => apiService.unfavorite({ id, credentials }) + const retweet = (id) => apiService.retweet({ id, credentials }) + const unretweet = (id) => apiService.unretweet({ id, credentials }) + const search2 = ({ q, resolve, limit, offset, following }) => + apiService.search2({ credentials, q, resolve, limit, offset, following }) + const searchUsers = (query) => apiService.searchUsers({ query, credentials }) const backendInteractorServiceInstance = { fetchStatus, @@ -137,19 +164,22 @@ const backendInteractorService = (credentials) => { unblockUser, fetchUser, fetchUserRelationship, - fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, startFetchingTimeline, startFetchingNotifications, fetchMutes, muteUser, unmuteUser, + subscribeUser, + unsubscribeUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, fetchPinnedStatuses, pinOwnStatus, unpinOwnStatus, + muteConversation, + unmuteConversation, tagUser, untagUser, addRight, @@ -162,21 +192,30 @@ const backendInteractorService = (credentials) => { updateBg, updateBanner, updateProfile, - externalProfile, importBlocks, importFollows, deleteAccount, changePassword, + fetchSettingsMFA, + generateMfaBackupCodes, + mfaSetupOTP, + mfaConfirmOTP, + mfaDisableOTP, fetchFollowRequests, approveUser, denyUser, + vote, + fetchPoll, fetchFavoritedByUsers, fetchRebloggedByUsers, reportUser, favorite, unfavorite, retweet, - unretweet + unretweet, + updateNotificationSettings, + search2, + searchUsers } return backendInteractorServiceInstance diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 7576c518..d1b17c61 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -59,7 +59,7 @@ const srgbToLinear = (srgb) => { * @returns {Number} relative luminance */ const relativeLuminance = (srgb) => { - const {r, g, b} = srgbToLinear(srgb) + const { r, g, b } = srgbToLinear(srgb) return 0.2126 * r + 0.7152 * g + 0.0722 * b } diff --git a/src/services/completion/completion.js b/src/services/completion/completion.js index 11c45867..df83d03d 100644 --- a/src/services/completion/completion.js +++ b/src/services/completion/completion.js @@ -8,7 +8,7 @@ export const wordAtPosition = (str, pos) => { const words = splitIntoWords(str) const wordsWithPosition = addPositionToWords(words) - return find(wordsWithPosition, ({start, end}) => start <= pos && end > pos) + return find(wordsWithPosition, ({ start, end }) => start <= pos && end > pos) } export const addPositionToWords = (words) => { diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js new file mode 100644 index 00000000..32e13bca --- /dev/null +++ b/src/services/date_utils/date_utils.js @@ -0,0 +1,45 @@ +export const SECOND = 1000 +export const MINUTE = 60 * SECOND +export const HOUR = 60 * MINUTE +export const DAY = 24 * HOUR +export const WEEK = 7 * DAY +export const MONTH = 30 * DAY +export const YEAR = 365.25 * DAY + +export const relativeTime = (date, nowThreshold = 1) => { + if (typeof date === 'string') date = Date.parse(date) + const round = Date.now() > date ? Math.floor : Math.ceil + const d = Math.abs(Date.now() - date) + let r = { num: round(d / YEAR), key: 'time.years' } + if (d < nowThreshold * SECOND) { + r.num = 0 + r.key = 'time.now' + } else if (d < MINUTE) { + r.num = round(d / SECOND) + r.key = 'time.seconds' + } else if (d < HOUR) { + r.num = round(d / MINUTE) + r.key = 'time.minutes' + } else if (d < DAY) { + r.num = round(d / HOUR) + r.key = 'time.hours' + } else if (d < WEEK) { + r.num = round(d / DAY) + r.key = 'time.days' + } else if (d < MONTH) { + r.num = round(d / WEEK) + r.key = 'time.weeks' + } else if (d < YEAR) { + r.num = round(d / MONTH) + r.key = 'time.months' + } + // Remove plural form when singular + if (r.num === 1) r.key = r.key.slice(0, -1) + return r +} + +export const relativeTimeShort = (date, nowThreshold = 1) => { + const r = relativeTime(date, nowThreshold) + r.key += '_short' + return r +} diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 8e413584..5f45660d 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -33,6 +33,7 @@ export const parseUser = (data) => { if (masto) { output.screen_name = data.acct + output.statusnet_profile_url = data.url // There's nothing else to get if (mastoShort) { @@ -42,7 +43,7 @@ export const parseUser = (data) => { output.name = data.display_name output.name_html = addEmojis(data.display_name, data.emojis) - // output.description = ??? missing + output.description = data.note output.description_html = addEmojis(data.note, data.emojis) // Utilize avatar_static for gif avatars? @@ -56,22 +57,49 @@ export const parseUser = (data) => { output.bot = data.bot - output.statusnet_profile_url = data.url - if (data.pleroma) { const relationship = data.pleroma.relationship + output.background_image = data.pleroma.background_image + output.token = data.pleroma.chat_token + if (relationship) { output.follows_you = relationship.followed_by + output.requested = relationship.requested output.following = relationship.following output.statusnet_blocking = relationship.blocking output.muted = relationship.muting + output.showing_reblogs = relationship.showing_reblogs + output.subscribed = relationship.subscribing } + output.hide_follows = data.pleroma.hide_follows + output.hide_followers = data.pleroma.hide_followers + output.hide_follows_count = data.pleroma.hide_follows_count + output.hide_followers_count = data.pleroma.hide_followers_count + output.rights = { moderator: data.pleroma.is_moderator, admin: data.pleroma.is_admin } + // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi + if (output.rights.admin) { + output.role = 'admin' + } else if (output.rights.moderator) { + output.role = 'moderator' + } else { + output.role = 'member' + } + } + + if (data.source) { + output.description = data.source.note + output.default_scope = data.source.privacy + if (data.source.pleroma) { + output.no_rich_text = data.source.pleroma.no_rich_text + output.show_role = data.source.pleroma.show_role + output.discoverable = data.source.pleroma.discoverable + } } // TODO: handle is_local @@ -106,8 +134,6 @@ export const parseUser = (data) => { output.muted = data.muted - // QVITTER ONLY FOR NOW - // Really only applies to logged in user, really.. I THINK if (data.rights) { output.rights = { moderator: data.rights.delete_others_notice, @@ -118,6 +144,8 @@ export const parseUser = (data) => { output.default_scope = data.default_scope output.hide_follows = data.hide_follows output.hide_followers = data.hide_followers + output.hide_follows_count = data.hide_follows_count + output.hide_followers_count = data.hide_followers_count output.background_image = data.background_image // on mastoapi this info is contained in a "relationship" output.following = data.following @@ -131,19 +159,20 @@ export const parseUser = (data) => { output.statuses_count = data.statuses_count output.friendIds = [] output.followerIds = [] - output.pinnedStatuseIds = [] + output.pinnedStatusIds = [] if (data.pleroma) { output.follow_request_count = data.pleroma.follow_request_count - } - if (data.pleroma) { output.tags = data.pleroma.tags output.deactivated = data.pleroma.deactivated + + output.notification_settings = data.pleroma.notification_settings } output.tags = output.tags || [] output.rights = output.rights || {} + output.notification_settings = output.notification_settings || {} return output } @@ -168,9 +197,11 @@ export const parseAttachment = (data) => { return output } export const addEmojis = (string, emojis) => { + const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g return emojis.reduce((acc, emoji) => { + const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') return acc.replace( - new RegExp(`:${emoji.shortcode}:`, 'g'), + new RegExp(`:${regexSafeShortCode}:`, 'g'), `<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />` ) }, string) @@ -192,6 +223,8 @@ export const parseStatus = (data) => { output.statusnet_html = addEmojis(data.content, data.emojis) + output.tags = data.tags + if (data.pleroma) { const { pleroma } = data output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content @@ -199,6 +232,7 @@ export const parseStatus = (data) => { output.statusnet_conversation_id = data.pleroma.conversation_id output.is_local = pleroma.local output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct + output.thread_muted = pleroma.thread_muted } else { output.text = data.content output.summary = data.spoiler_text @@ -214,7 +248,9 @@ export const parseStatus = (data) => { output.summary_html = addEmojis(data.spoiler_text, data.emojis) output.external_url = data.url + output.poll = data.poll output.pinned = data.pinned + output.muted = data.muted } else { output.favorited = data.favorited output.fave_num = data.fave_num diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 548f3c68..590552da 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -1,3 +1,5 @@ +import { humanizeErrors } from '../../modules/errors' + export function StatusCodeError (statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode @@ -12,3 +14,36 @@ export function StatusCodeError (statusCode, body, options, response) { } StatusCodeError.prototype = Object.create(Error.prototype) StatusCodeError.prototype.constructor = StatusCodeError + +export class RegistrationError extends Error { + constructor (error) { + super() + if (Error.captureStackTrace) { + Error.captureStackTrace(this) + } + + try { + // the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors + if (typeof error === 'string') { + error = JSON.parse(error) + if (error.hasOwnProperty('error')) { + error = JSON.parse(error.error) + } + } + + if (typeof error === 'object') { + // replace ap_id with username + if (error.ap_id) { + error.username = error.ap_id + delete error.ap_id + } + this.message = humanizeErrors(error) + } else { + this.message = error + } + } catch (e) { + // can't parse it, so just treat it like a string + this.message = error + } + } +} diff --git a/src/services/file_size_format/file_size_format.js b/src/services/file_size_format/file_size_format.js index add56ee0..7e6cd4d7 100644 --- a/src/services/file_size_format/file_size_format.js +++ b/src/services/file_size_format/file_size_format.js @@ -9,7 +9,7 @@ const fileSizeFormat = (num) => { exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1) num = (num / Math.pow(1024, exponent)).toFixed(2) * 1 unit = units[exponent] - return {num: num, unit: unit} + return { num: num, unit: unit } } const fileSizeFormatService = { fileSizeFormat diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index b2486e7c..598cb5f7 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -2,33 +2,26 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { setTimeout(() => { store.state.api.backendInteractor.fetchUser({ id: user.id }) .then((user) => store.commit('addNewUsers', [user])) - .then(() => resolve([user.following, attempt])) + .then(() => resolve([user.following, user.requested, user.locked, attempt])) .catch((e) => reject(e)) }, 500) -}).then(([following, attempt]) => { - if (!following && attempt <= 3) { +}).then(([following, sent, locked, attempt]) => { + if (!following && !(locked && sent) && attempt <= 3) { // If we BE reports that we still not following that user - retry, // increment attempts by one - return fetchUser(++attempt, user, store) - } else { - // If we run out of attempts, just return whatever status is. - return following + fetchUser(++attempt, user, store) } }) export const requestFollow = (user, store) => new Promise((resolve, reject) => { - store.state.api.backendInteractor.followUser(user.id) + store.state.api.backendInteractor.followUser({ id: user.id }) .then((updated) => { store.commit('updateUserRelationship', [updated]) - // For locked users we just mark it that we sent the follow request - if (updated.locked) { - resolve({ sent: true }) - } - - if (updated.following) { - // If we get result immediately, just stop. - resolve({ sent: false }) + if (updated.following || (user.locked && user.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 @@ -39,14 +32,8 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => { // Recursive Promise, it will call itself up to 3 times. return fetchUser(1, user, store) - .then((following) => { - if (following) { - // We confirmed and everything's good. - resolve({ sent: false }) - } else { - // If after all the tries, just treat it as if user is locked - resolve({ sent: false }) - } + .then(() => { + resolve() }) }) }) diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js index 125ff3e1..786740b7 100644 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -8,7 +8,7 @@ const fetchAndUpdate = ({ store, credentials }) => { .catch(() => {}) } -const startFetching = ({credentials, store}) => { +const startFetching = ({ credentials, store }) => { fetchAndUpdate({ credentials, store }) const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) return setInterval(boundFetchAndUpdate, 10000) diff --git a/src/services/new_api/mfa.js b/src/services/new_api/mfa.js new file mode 100644 index 00000000..cbba06d5 --- /dev/null +++ b/src/services/new_api/mfa.js @@ -0,0 +1,38 @@ +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 mfa = { + verifyOTPCode, + verifyRecoveryCode +} + +export default mfa diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js index 9e656507..d0d18c03 100644 --- a/src/services/new_api/oauth.js +++ b/src/services/new_api/oauth.js @@ -1,51 +1,57 @@ -import {reduce} from 'lodash' +import { reduce } from 'lodash' + +const REDIRECT_URI = `${window.location.origin}/oauth-callback` + +export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) => { + if (clientId && clientSecret) { + return Promise.resolve({ clientId, clientSecret }) + } -const getOrCreateApp = ({oauth, instance}) => { const url = `${instance}/api/v1/apps` const form = new window.FormData() - form.append('client_name', `PleromaFE_${Math.random()}`) - form.append('redirect_uris', `${window.location.origin}/oauth-callback`) + form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`) + form.append('redirect_uris', REDIRECT_URI) form.append('scopes', 'read write follow') return window.fetch(url, { method: 'POST', body: form - }).then((data) => data.json()) + }) + .then((data) => data.json()) + .then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret })) + .then((app) => commit('setClientData', app) || app) } -const login = (args) => { - getOrCreateApp(args).then((app) => { - args.commit('setClientData', app) - - const data = { - response_type: 'code', - client_id: app.client_id, - redirect_uri: app.redirect_uri, - scope: 'read write follow' - } - const dataString = reduce(data, (acc, v, k) => { - const encoded = `${k}=${encodeURIComponent(v)}` - if (!acc) { - return encoded - } else { - return `${acc}&${encoded}` - } - }, false) +const login = ({ instance, clientId }) => { + const data = { + response_type: 'code', + client_id: clientId, + redirect_uri: REDIRECT_URI, + scope: 'read write follow' + } - // Do the redirect... - const url = `${args.instance}/oauth/authorize?${dataString}` + const dataString = reduce(data, (acc, v, k) => { + const encoded = `${k}=${encodeURIComponent(v)}` + if (!acc) { + return encoded + } else { + return `${acc}&${encoded}` + } + }, false) - window.location.href = url - }) + // Do the redirect... + const url = `${instance}/oauth/authorize?${dataString}` + + window.location.href = url } -const getTokenWithCredentials = ({app, instance, username, password}) => { +const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, password }) => { const url = `${instance}/oauth/token` const form = new window.FormData() - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) + form.append('client_id', clientId) + form.append('client_secret', clientSecret) form.append('grant_type', 'password') form.append('username', username) form.append('password', password) @@ -56,12 +62,12 @@ const getTokenWithCredentials = ({app, instance, username, password}) => { }).then((data) => data.json()) } -const getToken = ({app, instance, code}) => { +const getToken = ({ clientId, clientSecret, instance, code }) => { const url = `${instance}/oauth/token` const form = new window.FormData() - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) + 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`) @@ -69,6 +75,67 @@ const getToken = ({app, instance, code}) => { 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((data) => data.json()) +} +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()) } @@ -76,7 +143,10 @@ const oauth = { login, getToken, getTokenWithCredentials, - getOrCreateApp + getOrCreateApp, + verifyOTPCode, + verifyRecoveryCode, + revokeToken } export default oauth diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js new file mode 100644 index 00000000..43199625 --- /dev/null +++ b/src/services/new_api/password_reset.js @@ -0,0 +1,18 @@ +import { reduce } from 'lodash' + +const MASTODON_PASSWORD_RESET_URL = `/auth/password` + +const resetPassword = ({ instance, email }) => { + const params = { email } + const query = reduce(params, (acc, v, k) => { + const encoded = `${k}=${encodeURIComponent(v)}` + return `${acc}&${encoded}` + }, '') + const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}` + + return window.fetch(url, { + method: 'POST' + }) +} + +export default resetPassword diff --git a/src/services/new_api/user_search.js b/src/services/new_api/user_search.js deleted file mode 100644 index 869afa9c..00000000 --- a/src/services/new_api/user_search.js +++ /dev/null @@ -1,19 +0,0 @@ -import utils from './utils.js' -import { parseUser } from '../entity_normalizer/entity_normalizer.service.js' - -const search = ({query, store}) => { - return utils.request({ - store, - url: '/api/v1/accounts/search', - params: { - q: query - } - }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} -const UserSearch = { - search -} - -export default UserSearch diff --git a/src/services/new_api/utils.js b/src/services/new_api/utils.js deleted file mode 100644 index 078f392f..00000000 --- a/src/services/new_api/utils.js +++ /dev/null @@ -1,36 +0,0 @@ -const queryParams = (params) => { - return Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) - .join('&') -} - -const headers = (store) => { - const accessToken = store.state.oauth.token - if (accessToken) { - return {'Authorization': `Bearer ${accessToken}`} - } else { - return {} - } -} - -const request = ({method = 'GET', url, params, store}) => { - const instance = store.state.instance.server - let fullUrl = `${instance}${url}` - - if (method === 'GET' && params) { - fullUrl = fullUrl + `?${queryParams(params)}` - } - - return window.fetch(fullUrl, { - method, - headers: headers(store), - credentials: 'same-origin' - }) -} - -const utils = { - queryParams, - request -} - -export default utils diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 8afd114e..7021adbd 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -25,12 +25,14 @@ const sortById = (a, b) => { } } -export const visibleNotificationsFromStore = store => { +export const visibleNotificationsFromStore = (store, types) => { // map is just to clone the array since sort mutates it and it causes some issues let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) sortedNotifications = sortBy(sortedNotifications, 'seen') - return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type)) + return sortedNotifications.filter( + (notification) => (types || visibleTypes(store)).includes(notification.type) + ) } export const unseenNotificationsFromStore = store => - filter(visibleNotificationsFromStore(store), ({seen}) => !seen) + filter(visibleNotificationsFromStore(store), ({ seen }) => !seen) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 60c497ae..47008026 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,15 +1,19 @@ import apiService from '../api/api.service.js' -const update = ({store, notifications, older}) => { +const update = ({ store, notifications, older }) => { store.dispatch('setNotificationsError', { value: false }) store.dispatch('addNewNotifications', { notifications, older }) } -const fetchAndUpdate = ({store, credentials, older = false}) => { +const fetchAndUpdate = ({ store, credentials, older = false }) => { const args = { credentials } + const { getters } = store const rootState = store.rootState || store.state const timelineData = rootState.statuses.notifications + const hideMutedPosts = getters.mergedConfig.hideMutedPosts + + args['withMuted'] = !hideMutedPosts args['timeline'] = 'notifications' if (older) { @@ -45,7 +49,7 @@ const fetchNotifications = ({ store, args, older }) => { .catch(() => store.dispatch('setNotificationsError', { value: true })) } -const startFetching = ({credentials, store}) => { +const startFetching = ({ credentials, store }) => { fetchAndUpdate({ credentials, store }) const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) // Initially there's set flag to silence all desktop notifications so diff --git a/src/services/offset_finder/offset_finder.service.js b/src/services/offset_finder/offset_finder.service.js new file mode 100644 index 00000000..9034f8c8 --- /dev/null +++ b/src/services/offset_finder/offset_finder.service.js @@ -0,0 +1,31 @@ +export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => { + const result = { + top: top + child.offsetTop, + left: left + child.offsetLeft + } + if (!ignorePadding && child !== window) { + const { topPadding, leftPadding } = findPadding(child) + result.top += ignorePadding ? 0 : topPadding + result.left += ignorePadding ? 0 : leftPadding + } + + if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) { + return findOffset(child.offsetParent, parent, result, false) + } else { + if (parent !== window) { + const { topPadding, leftPadding } = findPadding(parent) + result.top += topPadding + result.left += leftPadding + } + return result + } +} + +const findPadding = (el) => { + const topPaddingStr = window.getComputedStyle(el)['padding-top'] + const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2)) + const leftPaddingStr = window.getComputedStyle(el)['padding-left'] + const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2)) + + return { topPadding, leftPadding } +} diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index e70b0f26..9e904d3a 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -1,10 +1,19 @@ import { map } from 'lodash' import apiService from '../api/api.service.js' -const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => { +const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => { const mediaIds = map(media, 'id') - return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) + return apiService.postStatus({ + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + visibility, + sensitive, + mediaIds, + inReplyToStatusId, + contentType, + poll }) .then((data) => { if (!data.error) { store.dispatch('addNewStatuses', { diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index d0b6ccbf..1cf7edc3 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -22,7 +22,7 @@ const setStyle = (href, commit) => { ***/ const head = document.head const body = document.body - body.style.display = 'none' + body.classList.add('hidden') const cssEl = document.createElement('link') cssEl.setAttribute('rel', 'stylesheet') cssEl.setAttribute('href', href) @@ -46,7 +46,7 @@ const setStyle = (href, commit) => { head.appendChild(styleEl) // const styleSheet = styleEl.sheet - body.style.display = 'initial' + body.classList.remove('hidden') } cssEl.addEventListener('load', setDynamic) @@ -75,7 +75,7 @@ const applyTheme = (input, commit) => { const { rules, theme } = generatePreset(input) const head = document.head const body = document.body - body.style.display = 'none' + body.classList.add('hidden') const styleEl = document.createElement('style') head.appendChild(styleEl) @@ -86,7 +86,7 @@ const applyTheme = (input, commit) => { styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max') styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max') - body.style.display = 'initial' + body.classList.remove('hidden') // commit('setOption', { name: 'colors', value: htmlColors }) // commit('setOption', { name: 'radii', value: radii }) @@ -202,6 +202,7 @@ const generateColors = (input) => { colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) colors.faintLink = col.faintLink || Object.assign({}, col.link) + colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) colors.icon = mixrgb(colors.bg, colors.text) @@ -238,12 +239,12 @@ const generateColors = (input) => { }) const htmlColors = Object.entries(colors) - .reduce((acc, [k, v]) => { - if (!v) return acc - acc.solid[k] = rgb2hex(v) - acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v) - return acc - }, { complete: {}, solid: {} }) + .reduce((acc, [k, v]) => { + if (!v) return acc + acc.solid[k] = rgb2hex(v) + acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v) + return acc + }, { complete: {}, solid: {} }) return { rules: { colors: Object.entries(htmlColors.complete) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 8e954cdf..9eb30c2d 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -2,7 +2,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' -const update = ({store, statuses, timeline, showImmediately, userId}) => { +const update = ({ store, statuses, timeline, showImmediately, userId }) => { const ccTimeline = camelCase(timeline) store.dispatch('setError', { value: false }) @@ -15,13 +15,21 @@ const update = ({store, statuses, timeline, showImmediately, userId}) => { }) } -const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => { +const fetchAndUpdate = ({ + store, + credentials, + timeline = 'friends', + older = false, + showImmediately = false, + userId = false, + tag = false, + until +}) => { const args = { timeline, credentials } const rootState = store.rootState || store.state + const { getters } = store const timelineData = rootState.statuses.timelines[camelCase(timeline)] - const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' - ? rootState.instance.hideMutedPosts - : rootState.config.hideMutedPosts + const hideMutedPosts = getters.mergedConfig.hideMutedPosts if (older) { args['until'] = until || timelineData.minId @@ -40,17 +48,17 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } - update({store, statuses, timeline, showImmediately, userId}) + update({ store, statuses, timeline, showImmediately, userId }) return statuses }, () => store.dispatch('setError', { value: true })) } -const startFetching = ({timeline = 'friends', credentials, store, userId = false, tag = false}) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId - fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag}) + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, tag }) const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag }) return setInterval(boundFetchAndUpdate, 10000) } diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js index f6ddfb9c..b91c0f78 100644 --- a/src/services/user_highlighter/user_highlighter.js +++ b/src/services/user_highlighter/user_highlighter.js @@ -1,7 +1,7 @@ import { hex2rgb } from '../color_convert/color_convert.js' const highlightStyle = (prefs) => { if (prefs === undefined) return - const {color, type} = prefs + const { color, type } = prefs if (typeof color !== 'string') return const rgb = hex2rgb(color) if (rgb == null) return diff --git a/src/services/version/version.service.js b/src/services/version/version.service.js index a750b0dd..2e11bf3a 100644 --- a/src/services/version/version.service.js +++ b/src/services/version/version.service.js @@ -1,6 +1,6 @@ export const extractCommit = versionString => { - const regex = /-g(\w+)$/i + const regex = /-g(\w+)/i const matches = versionString.match(regex) return matches ? matches[1] : '' } |
