Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85830340
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
72 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/.eslintrc.js b/.eslintrc.js
index b526df6794..7d09020831 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,47 +1,46 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'standard',
'plugin:vue/recommended'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
// Webpack 4 update commit, most of these probably should be fixed and removed in a separate MR
// A lot of errors come from .vue files that are now properly linted
'vue/valid-v-if': 1,
'vue/use-v-on-exact': 1,
'vue/no-parsing-error': 1,
'vue/require-v-for-key': 1,
'vue/valid-v-for': 1,
'vue/require-prop-types': 1,
'vue/no-use-v-if-with-v-for': 1,
'indent': 1,
- 'import/first': 1, // ????
- 'import-first': 1,
+ 'import/first': 1,
'object-curly-spacing': 1,
'prefer-promise-reject-errors': 1,
'eol-last': 1,
'no-return-await': 1,
'no-multi-spaces': 1,
'no-trailing-spaces': 1,
'no-unused-expressions': 1,
'no-mixed-operators': 1,
'camelcase': 1,
'no-multiple-empty-lines': 1
}
}
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 7510e562bd..c271d413f2 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -1,293 +1,306 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils'
+import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
+import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
const getStatusnetConfig = async ({ store }) => {
try {
const res = await window.fetch('/api/statusnet/config.json')
if (res.ok) {
const data = await res.json()
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setInstanceOption', { name: 'server', value: server })
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
// TODO: default values for this stuff, added if to not make it break on
// my dev config out of the box.
if (uploadlimit) {
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
}
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
}
return data.site.pleromafe
} else {
throw (res)
}
} catch (error) {
console.error('Could not load statusnet config, potentially fatal')
console.error(error)
}
}
const getStaticConfig = async () => {
try {
const res = await window.fetch('/static/config.json')
if (res.ok) {
return res.json()
} else {
throw (res)
}
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(error)
return {}
}
}
const setSettings = async ({ apiConfig, staticConfig, store }) => {
const overrides = window.___pleromafe_dev_overrides || {}
const env = window.___pleromafe_mode.NODE_ENV
// This takes static config and overrides properties that are present in apiConfig
let config = {}
if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig)
} else {
config = Object.assign({}, staticConfig, apiConfig)
}
const copyInstanceOption = (name) => {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
store.dispatch('setInstanceOption', {
name: 'logoMask',
value: typeof config.logoMask === 'undefined'
? true
: config.logoMask
})
store.dispatch('setInstanceOption', {
name: 'logoMargin',
value: typeof config.logoMargin === 'undefined'
? 0
: config.logoMargin
})
store.commit('authFlow/setInitialStrategy', config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('formattingOptionsEnabled')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks')
copyInstanceOption('showFeaturesPanel')
if ((config.chatDisabled)) {
store.dispatch('disableChat')
} else {
store.dispatch('initializeSocket')
}
return store.dispatch('setTheme', config['theme'])
}
const getTOS = async ({ store }) => {
try {
const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load TOS")
console.warn(e)
}
}
const getInstancePanel = async ({ store }) => {
try {
const res = await window.fetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load instance panel")
console.warn(e)
}
}
const getStaticEmoji = async ({ store }) => {
try {
const res = await window.fetch('/static/emoji.json')
if (res.ok) {
const values = await res.json()
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load static emoji")
console.warn(e)
}
}
// This is also used to indicate if we have a 'pleroma backend' or not.
// Somewhat weird, should probably be somewhere else.
const getCustomEmoji = async ({ store }) => {
try {
const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: values[key].image_url || values[key] }
})
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
} else {
throw (res)
}
} catch (e) {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
console.warn("Can't load custom emojis, maybe not a Pleroma instance?")
console.warn(e)
}
}
+const getAppSecret = async ({ store }) => {
+ const { state, commit } = store
+ const { oauth, instance } = state
+ return getOrCreateApp({ ...oauth, instance: instance.server, commit })
+ .then((app) => getClientToken({ ...app, instance: instance.server }))
+ .then((token) => {
+ commit('setAppToken', token.access_token)
+ commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
+ })
+}
+
const getNodeInfo = async ({ store }) => {
try {
const res = await window.fetch('/nodeinfo/2.0.json')
if (res.ok) {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
} else {
throw (res)
}
} catch (e) {
console.warn('Could not load nodeinfo')
console.warn(e)
}
}
const setConfig = async ({ store }) => {
// apiConfig, staticConfig
const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
- await setSettings({ store, apiConfig, staticConfig })
+ await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
}
const checkOAuthToken = async ({ store }) => {
return new Promise(async (resolve, reject) => {
- if (store.state.oauth.token) {
+ if (store.getters.getUserToken()) {
try {
- await store.dispatch('loginUser', store.state.oauth.token)
+ await store.dispatch('loginUser', store.getters.getUserToken())
} catch (e) {
console.log(e)
}
}
resolve()
})
}
const afterStoreSetup = async ({ store, i18n }) => {
if (store.state.config.customTheme) {
// This is a hack to deal with async loading of config.json and themes
// See: style_setter.js, setPreset()
window.themeLoaded = true
store.dispatch('setOption', {
name: 'customTheme',
value: store.state.config.customTheme
})
}
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
// Now we can try getting the server settings and logging in
await Promise.all([
checkOAuthToken({ store }),
setConfig({ store }),
getTOS({ store }),
getInstancePanel({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store })
])
const router = new VueRouter({
mode: 'history',
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
/* eslint-disable no-new */
return new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
}
export default afterStoreSetup
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 0d76af0e27..9321464629 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -1,75 +1,82 @@
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import oauthApi from '../../services/new_api/oauth.js'
const LoginForm = {
data: () => ({
user: {},
error: false
}),
computed: {
isPasswordAuth () { return this.requiredPassword },
isTokenAuth () { return this.requiredToken },
...mapState({
registrationOpen: state => state.instance.registrationOpen,
instance: state => state.instance,
loggingIn: state => state.users.loggingIn,
oauth: state => state.oauth
}),
...mapGetters(
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
)
},
methods: {
...mapMutations('authFlow', ['requireMFA']),
...mapActions({ login: 'authFlow/login' }),
submit () {
this.isTokenAuth ? this.submitToken() : this.submitPassword()
},
submitToken () {
- oauthApi.login({
- oauth: this.oauth,
+ const { clientId } = this.oauth
+ const data = {
+ clientId,
instance: this.instance.server,
commit: this.$store.commit
- })
+ }
+
+ oauthApi.getOrCreateApp(data)
+ .then((app) => { oauthApi.login({ ...app, ...data }) })
},
submitPassword () {
+ const { clientId } = this.oauth
const data = {
+ clientId,
oauth: this.oauth,
- instance: this.instance.server
+ instance: this.instance.server,
+ commit: this.$store.commit
}
this.error = false
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
- app,
+ ...app,
instance: data.instance,
username: this.user.username,
password: this.user.password
}
).then((result) => {
if (result.error) {
if (result.error === 'mfa_required') {
this.requireMFA({app: app, settings: result})
} else {
this.error = result.error
this.focusOnPasswordInput()
}
return
}
this.login(result).then(() => {
this.$router.push({name: 'friends'})
})
})
})
},
clearError () { this.error = false },
focusOnPasswordInput () {
let passwordInput = this.$refs.passwordInput
passwordInput.focus()
passwordInput.setSelectionRange(0, passwordInput.value.length)
}
}
}
export default LoginForm
diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js
index e3d45ee12d..2c6ca235a1 100644
--- a/src/components/oauth_callback/oauth_callback.js
+++ b/src/components/oauth_callback/oauth_callback.js
@@ -1,20 +1,22 @@
import oauth from '../../services/new_api/oauth.js'
const oac = {
props: ['code'],
mounted () {
if (this.code) {
+ const { clientId } = this.$store.state.oauth
+
oauth.getToken({
- app: this.$store.state.oauth,
+ clientId,
instance: this.$store.state.instance.server,
code: this.code
}).then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
- this.$router.push({name: 'friends'})
+ this.$router.push({ name: 'friends' })
})
}
}
}
export default oac
diff --git a/src/modules/oauth.js b/src/modules/oauth.js
index 144ff830a0..11cb10fe3a 100644
--- a/src/modules/oauth.js
+++ b/src/modules/oauth.js
@@ -1,18 +1,41 @@
const oauth = {
state: {
- client_id: false,
- client_secret: false,
- token: false
+ clientId: false,
+ clientSecret: false,
+ /* App token is authentication for app without any user, used mostly for
+ * MastoAPI's registration of new users, stored so that we can fall back to
+ * it on logout
+ */
+ appToken: false,
+ /* User token is authentication for app with user, this is for every calls
+ * that need authorized user to be successful (i.e. posting, liking etc)
+ */
+ userToken: false
},
mutations: {
- setClientData (state, data) {
- state.client_id = data.client_id
- state.client_secret = data.client_secret
+ setClientData (state, { clientId, clientSecret }) {
+ state.clientId = clientId
+ state.clientSecret = clientSecret
+ },
+ setAppToken (state, token) {
+ state.appToken = token
},
setToken (state, token) {
- state.token = token
+ state.userToken = token
+ }
+ },
+ getters: {
+ getToken: state => () => {
+ // state.token is userToken with older name, coming from persistent state
+ // added here for smoother transition, otherwise user will be logged out
+ return state.userToken || state.token || state.appToken
+ },
+ getUserToken: state => () => {
+ // state.token is userToken with older name, coming from persistent state
+ // added here for smoother transition, otherwise user will be logged out
+ return state.userToken || state.token
}
}
}
export default oauth
diff --git a/src/modules/users.js b/src/modules/users.js
index e72a657c83..739b8b9218 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,477 +1,466 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import userSearchApi from '../services/new_api/user_search.js'
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
-import oauthApi from '../services/new_api/oauth'
import { humanizeErrors } from './errors'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
if (!item) { return false }
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
merge(oldItem, item)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
arr.push(item)
set(obj, item.id, item)
if (item.screen_name && !item.screen_name.includes('@')) {
set(obj, item.screen_name.toLowerCase(), item)
}
return { item, new: true }
}
}
const getNotificationPermission = () => {
const Notification = window.Notification
if (!Notification) return Promise.resolve(null)
if (Notification.permission === 'default') return Notification.requestPermission()
return Promise.resolve(Notification.permission)
}
const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser(id)
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
})
}
const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor.unblockUser(id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, id) => {
return store.rootState.api.backendInteractor.muteUser(id)
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
})
}
const unmuteUser = (store, id) => {
return store.rootState.api.backendInteractor.unmuteUser(id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
set(user, 'muted', muted)
},
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
set(user, 'tags', newTags)
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
set(user, 'tags', newTags)
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
let newRights = user.rights
newRights[right] = value
set(user, 'rights', newRights)
},
updateActivationStatus (state, { user: { id }, status }) {
const user = state.usersObject[id]
set(user, 'deactivated', !status)
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
state.currentUser = merge(state.currentUser || {}, user)
},
clearCurrentUser (state) {
state.currentUser = false
state.lastLoginName = false
},
beginLogin (state) {
state.loggingIn = true
},
endLogin (state) {
state.loggingIn = false
},
saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = uniq(concat(user.friendIds, friendIds))
},
saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds, followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (user) {
set(user, 'friendIds', [])
}
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (user) {
set(user, 'followerIds', [])
}
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
const user = state.usersObject[relationship.id]
if (user) {
user.follows_you = relationship.followed_by
user.following = relationship.following
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
}
})
},
updateBlocks (state, blockedUsers) {
// Reset statusnet_blocking of all fetched users
each(state.users, (user) => { user.statusnet_blocking = false })
each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
addBlockId (state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
updateMutes (state, mutedUsers) {
// Reset muted of all fetched users
each(state.users, (user) => { user.muted = false })
each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds
},
addMuteId (state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
setPinned (state, status) {
const user = state.usersObject[status.user.id]
const index = user.pinnedStatuseIds.indexOf(status.id)
if (status.pinned && index === -1) {
user.pinnedStatuseIds.push(status.id)
} else if (!status.pinned && index !== -1) {
user.pinnedStatuseIds.splice(index, 1)
}
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id]
}
notification.from_profile = state.usersObject[notification.from_profile.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
set(user, 'highlight', highlighted)
},
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
},
signUpSuccess (state) {
state.signUpPending = false
},
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
}
}
export const getters = {
findUser: state => query => {
const result = state.usersObject[query]
// In case it's a screen_name, we can try searching case-insensitive
if (!result && typeof query === 'string') {
return state.usersObject[query.toLowerCase()]
}
return result
}
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
users: [],
usersObject: {},
signUpPending: false,
signUpErrors: []
}
const users = {
state: defaultState,
mutations,
getters,
actions: {
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlockIds', map(blocks, 'id'))
store.commit('updateBlocks', blocks)
return blocks
})
},
blockUser (store, id) {
return blockUser(store, id)
},
unblockUser (store, id) {
return unblockUser(store, id)
},
blockUsers (store, ids = []) {
return Promise.all(ids.map(id => blockUser(store, id)))
},
unblockUsers (store, ids = []) {
return Promise.all(ids.map(id => unblockUser(store, id)))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutes) => {
store.commit('updateMutes', mutes)
store.commit('saveMuteIds', map(mutes, 'id'))
return mutes
})
},
muteUser (store, id) {
return muteUser(store, id)
},
unmuteUser (store, id) {
return unmuteUser(store, id)
},
muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id)))
},
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
.then((friends) => {
commit('addNewUsers', friends)
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
},
fetchFollowers ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds)
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
.then((followers) => {
commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
clearFriends ({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
const isEnabled = store.rootState.config.webPushNotifications
const notificationVisibility = store.rootState.config.notificationVisibility
registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
},
unregisterPushNotifications (store) {
const token = store.state.currentUser.credentials
unregisterPushNotifications(token)
},
addNewUsers ({ commit }, users) {
commit('addNewUsers', users)
},
addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
store.commit('addNewUsers', retweetedUsers)
each(statuses, (status) => {
// Reconnect users to statuses
store.commit('setUserForStatus', status)
// Set pinned statuses to user
store.commit('setPinned', status)
})
each(compact(map(statuses, 'retweeted_status')), (status) => {
// Reconnect users to retweets
store.commit('setUserForStatus', status)
// Set pinned retweets to user
store.commit('setPinned', status)
})
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k, val]) => notificationIds.includes(k))
.map(([k, val]) => val)
// Reconnect users to notifications
each(relevantNotifications, (notification) => {
store.commit('setUserForNotification', notification)
})
},
searchUsers (store, query) {
// TODO: Move userSearch api into api.service
return userSearchApi.search({query, store: { state: store.rootState }})
.then((users) => {
store.commit('addNewUsers', users)
return users
})
},
async signUp (store, userInfo) {
store.commit('signUpPending')
let rootState = store.rootState
- let response = await rootState.api.backendInteractor.register(userInfo)
- if (response.ok) {
- const data = {
- oauth: rootState.oauth,
- instance: rootState.instance.server
- }
- let app = await oauthApi.getOrCreateApp(data)
- let result = await oauthApi.getTokenWithCredentials({
- app,
- instance: data.instance,
- username: userInfo.username,
- password: userInfo.password
- })
+ try {
+ let data = await rootState.api.backendInteractor.register(userInfo)
store.commit('signUpSuccess')
- store.commit('setToken', result.access_token)
- store.dispatch('loginUser', result.access_token)
- } else {
- const data = await response.json()
- let errors = JSON.parse(data.error)
+ store.commit('setToken', data.access_token)
+ store.dispatch('loginUser', data.access_token)
+ } catch (e) {
+ let errors = e.message
// replace ap_id with username
- if (errors.ap_id) {
- errors.username = errors.ap_id
- delete errors.ap_id
+ if (typeof errors === 'object') {
+ if (errors.ap_id) {
+ errors.username = errors.ap_id
+ delete errors.ap_id
+ }
+ errors = humanizeErrors(errors)
}
- errors = humanizeErrors(errors)
store.commit('signUpFailure', errors)
throw Error(errors)
}
},
async getCaptcha (store) {
return await store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromChat')
store.commit('setToken', false)
store.dispatch('stopFetching', 'friends')
- store.commit('setBackendInteractor', backendInteractorService())
+ store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications')
store.commit('clearNotifications')
store.commit('resetStatuses')
},
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
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 = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission))
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
if (user.token) {
store.dispatch('setWsToken', user.token)
// Initialize the chat socket.
store.dispatch('initializeSocket')
}
// Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
// Get user mutes
store.dispatch('fetchMutes')
// 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')
if (response.status === 401) {
reject('Wrong username or password')
} else {
reject('An error occurred, please try again')
}
}
commit('endLogin')
resolve()
})
.catch((error) => {
console.log(error)
commit('endLogin')
reject('Failed to connect to server, try again')
})
})
}
}
}
export default users
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 6e0d54cd43..05d968f7b3 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,867 +1,877 @@
/* eslint-env browser */
-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 GET_BACKGROUND_HACK = '/api/account/verify_credentials.json'
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`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
const MASTODON_UNRETWEET_URL = id => `/api/v1/statuses/${id}/unreblog`
const MASTODON_DELETE_URL = id => `/api/v1/statuses/${id}`
const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow`
const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow`
const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following`
const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers`
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
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 oldfetch = window.fetch
let fetch = (url, options) => {
options = options || {}
const baseUrl = ''
const fullUrl = baseUrl + url
options.credentials = 'same-origin'
return oldfetch(fullUrl, options)
}
const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => {
const options = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...headers
}
}
if (payload) {
options.body = JSON.stringify(payload)
}
if (credentials) {
options.headers = {
...options.headers,
...authHeaders(credentials)
}
}
return fetch(url, options)
.then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url, options }, response))
}
return resolve(json)
}))
})
}
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))
}
const updateBg = ({credentials, params}) => {
let url = BG_UPDATE_URL
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
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))
}
const updateProfile = ({credentials, params}) => {
return promisedRequest({
url: MASTODON_PROFILE_UPDATE_URL,
method: 'PATCH',
payload: params,
credentials
})
.then((data) => parseUser(data))
}
// Params needed:
// nickname
// email
// fullname
// password
// password_confirm
//
// Optional
// bio
// 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) => [response.ok, response])
+ .then(([ok, response]) => {
+ if (ok) {
+ return response.json()
+ } else {
+ return response.json().then((error) => { throw new Error(error) })
+ }
+ })
}
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
const authHeaders = (accessToken) => {
if (accessToken) {
return { 'Authorization': `Bearer ${accessToken}` }
} else {
return { }
}
}
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}) => {
let url = MASTODON_FOLLOW_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const unfollowUser = ({id, credentials}) => {
let url = MASTODON_UNFOLLOW_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const pinOwnStatus = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
.then((data) => parseStatus(data))
}
const unpinOwnStatus = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNPIN_OWN_STATUS(id), credentials, method: 'POST' })
.then((data) => parseStatus(data))
}
const blockUser = ({id, credentials}) => {
return fetch(MASTODON_BLOCK_USER_URL(id), {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
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}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const denyUser = ({id, credentials}) => {
let url = `${DENY_USER_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const fetchUser = ({id, credentials}) => {
let url = `${MASTODON_USER_URL}/${id}`
return promisedRequest({ url, credentials })
.then((data) => parseUser(data))
}
const fetchUserRelationship = ({id, credentials}) => {
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url }, response))
}
return resolve(json)
}))
})
}
const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
let url = MASTODON_FOLLOWING_URL(id)
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`
].filter(_ => _).join('&')
url = url + (args ? '?' + args : '')
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
const exportFriends = ({id, credentials}) => {
return new Promise(async (resolve, reject) => {
try {
let friends = []
let more = true
while (more) {
const maxId = friends.length > 0 ? last(friends).id : undefined
const users = await fetchFriends({id, maxId, credentials})
friends = concat(friends, users)
if (users.length === 0) {
more = false
}
}
resolve(friends)
} catch (err) {
reject(err)
}
})
}
const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
let url = MASTODON_FOLLOWERS_URL(id)
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`
].filter(_ => _).join('&')
url += args ? '?' + args : ''
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
const fetchFollowRequests = ({credentials}) => {
const url = FOLLOW_REQUESTS_URL
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
}
const fetchConversation = ({id, credentials}) => {
let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
return fetch(urlContext, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
throw new Error('Error fetching timeline', data)
})
.then((data) => data.json())
.then(({ancestors, descendants}) => ({
ancestors: ancestors.map(parseStatus),
descendants: descendants.map(parseStatus)
}))
}
const fetchStatus = ({id, credentials}) => {
let url = MASTODON_STATUS_URL(id)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
throw new Error('Error fetching timeline', data)
})
.then((data) => data.json())
.then((data) => parseStatus(data))
}
const tagUser = ({tag, credentials, ...options}) => {
const screenName = options.screen_name
const form = {
nicknames: [screenName],
tags: [tag]
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'PUT',
headers: headers,
body: JSON.stringify(form)
})
}
const untagUser = ({tag, credentials, ...options}) => {
const screenName = options.screen_name
const body = {
nicknames: [screenName],
tags: [tag]
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'DELETE',
headers: headers,
body: JSON.stringify(body)
})
}
const addRight = ({right, credentials, ...user}) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
method: 'POST',
headers: authHeaders(credentials),
body: {}
})
}
const deleteRight = ({right, credentials, ...user}) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
method: 'DELETE',
headers: authHeaders(credentials),
body: {}
})
}
const setActivationStatus = ({status, credentials, ...user}) => {
const screenName = user.screen_name
const body = {
status: status
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(ACTIVATION_STATUS_URL(screenName), {
method: 'PUT',
headers: headers,
body: JSON.stringify(body)
})
}
const deleteUser = ({credentials, ...user}) => {
const screenName = user.screen_name
const headers = authHeaders(credentials)
return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
method: 'DELETE',
headers: headers
})
}
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,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
tag: MASTODON_TAG_TIMELINE_URL
}
const isNotifications = timeline === 'notifications'
const params = []
let url = timelineUrls[timeline]
if (timeline === 'user' || timeline === 'media') {
url = url(userId)
}
if (since) {
params.push(['since_id', since])
}
if (until) {
params.push(['max_id', until])
}
if (tag) {
url = url(tag)
}
if (timeline === 'media') {
params.push(['only_media', 1])
}
if (timeline === 'public') {
params.push(['local', true])
}
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
params.push(['count', 20])
params.push(['with_muted', withMuted])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
throw new Error('Error fetching timeline', data)
})
.then((data) => data.json())
.then((data) => data.map(isNotifications ? parseNotification : parseStatus))
}
const fetchPinnedStatuses = ({ id, credentials }) => {
const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true'
return promisedRequest({ url, credentials })
.then((data) => data.map(parseStatus))
}
const verifyCredentials = (user) => {
return fetch(MASTODON_LOGIN_URL, {
headers: authHeaders(user)
})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {
error: response
}
}
})
.then((data) => data.error ? data : parseUser(data))
.then((mastoUser) => {
// REMOVE WHEN BE SUPPORTS background_image
return fetch(GET_BACKGROUND_HACK, {
method: 'POST',
headers: authHeaders(user)
})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {}
}
})
/* eslint-disable camelcase */
.then(({ background_image }) => ({
...mastoUser,
background_image
}))
/* eslint-enable camelcase */
})
}
const favorite = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_FAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unfavorite = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNFAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const retweet = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unretweet = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNRETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
const form = new FormData()
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 (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId)
}
return fetch(MASTODON_POST_STATUS_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {
error: response
}
}
})
.then((data) => data.error ? data : parseStatus(data))
}
const deleteStatus = ({ id, credentials }) => {
return fetch(MASTODON_DELETE_URL(id), {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const uploadMedia = ({formData, credentials}) => {
return fetch(MASTODON_MEDIA_UPLOAD_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((data) => data.json())
.then((data) => parseAttachment(data))
}
const importBlocks = ({file, credentials}) => {
const formData = new FormData()
formData.append('list', file)
return fetch(BLOCKS_IMPORT_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.ok)
}
const importFollows = ({file, credentials}) => {
const formData = new FormData()
formData.append('list', file)
return fetch(FOLLOW_IMPORT_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.ok)
}
const deleteAccount = ({credentials, password}) => {
const form = new FormData()
form.append('password', password)
return fetch(DELETE_ACCOUNT_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const changePassword = ({credentials, password, newPassword, newPasswordConfirmation}) => {
const form = new FormData()
form.append('password', password)
form.append('new_password', newPassword)
form.append('new_password_confirmation', newPasswordConfirmation)
return fetch(CHANGE_PASSWORD_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const settingsMFA = ({credentials}) => {
return fetch(MFA_SETTINGS_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const mfaDisableOTP = ({credentials, password}) => {
const form = new FormData()
form.append('password', password)
return fetch(MFA_DISABLE_OTP_URL, {
body: form,
method: 'DELETE',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const mfaConfirmOTP = ({credentials, password, token}) => {
const form = new FormData()
form.append('password', password)
form.append('code', token)
return fetch(MFA_CONFIRM_OTP_URL, {
body: form,
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const mfaSetupOTP = ({credentials}) => {
return fetch(MFA_SETUP_OTP_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const generateMfaBackupCodes = ({credentials}) => {
return fetch(MFA_BACKUP_CODES_URL, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const fetchMutes = ({credentials}) => {
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
.then((users) => users.map(parseUser))
}
const muteUser = ({id, credentials}) => {
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' })
}
const unmuteUser = ({id, credentials}) => {
return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
}
const fetchBlocks = ({credentials}) => {
return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
.then((users) => users.map(parseUser))
}
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => {
if (data.ok) {
return data.json()
}
throw new Error('Error fetching auth tokens', data)
})
}
const revokeOAuthToken = ({id, credentials}) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const markNotificationsAsSeen = ({id, credentials}) => {
const body = new FormData()
body.append('latest_id', id)
return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const fetchFavoritedByUsers = ({id}) => {
return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
}
const fetchRebloggedByUsers = ({id}) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
}
const reportUser = ({credentials, userId, statusIds, comment, forward}) => {
return promisedRequest({
url: MASTODON_REPORT_USER_URL,
method: 'POST',
payload: {
'account_id': userId,
'status_ids': statusIds,
comment,
forward
},
credentials
})
}
const apiService = {
verifyCredentials,
fetchTimeline,
fetchPinnedStatuses,
fetchConversation,
fetchStatus,
fetchFriends,
exportFriends,
fetchFollowers,
followUser,
unfollowUser,
pinOwnStatus,
unpinOwnStatus,
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
favorite,
unfavorite,
retweet,
unretweet,
postStatus,
deleteStatus,
uploadMedia,
fetchMutes,
muteUser,
unmuteUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
tagUser,
untagUser,
deleteUser,
addRight,
deleteRight,
setActivationStatus,
register,
getCaptcha,
updateAvatar,
updateBg,
updateProfile,
updateBanner,
externalProfile,
importBlocks,
importFollows,
deleteAccount,
changePassword,
settingsMFA,
mfaDisableOTP,
generateMfaBackupCodes,
mfaSetupOTP,
mfaConfirmOTP,
fetchFollowRequests,
approveUser,
denyUser,
suggestions,
markNotificationsAsSeen,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,
updateNotificationSettings
}
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 674b7516ba..07093b5cfa 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -1,196 +1,196 @@
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 fetchConversation = ({id}) => {
return apiService.fetchConversation({id, credentials})
}
const fetchFriends = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFriends({id, maxId, sinceId, limit, credentials})
}
const exportFriends = ({id}) => {
return apiService.exportFriends({id, credentials})
}
const fetchFollowers = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials})
}
const fetchUser = ({id}) => {
return apiService.fetchUser({id, credentials})
}
const fetchUserRelationship = ({id}) => {
return apiService.fetchUserRelationship({id, credentials})
}
const followUser = (id) => {
return apiService.followUser({credentials, id})
}
const unfollowUser = (id) => {
return apiService.unfollowUser({credentials, id})
}
const blockUser = (id) => {
return apiService.blockUser({credentials, id})
}
const unblockUser = (id) => {
return apiService.unblockUser({credentials, id})
}
const approveUser = (id) => {
return apiService.approveUser({credentials, id})
}
const denyUser = (id) => {
return apiService.denyUser({credentials, id})
}
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
}
const startFetchingNotifications = ({ store }) => {
return notificationsFetcher.startFetching({ store, credentials })
}
const tagUser = ({screen_name}, tag) => {
return apiService.tagUser({screen_name, tag, credentials})
}
const untagUser = ({screen_name}, tag) => {
return apiService.untagUser({screen_name, tag, credentials})
}
const addRight = ({screen_name}, right) => {
return apiService.addRight({screen_name, right, credentials})
}
const deleteRight = ({screen_name}, right) => {
return apiService.deleteRight({screen_name, right, credentials})
}
const setActivationStatus = ({screen_name}, status) => {
return apiService.setActivationStatus({screen_name, status, credentials})
}
const deleteUser = ({screen_name}) => {
return apiService.deleteUser({screen_name, credentials})
}
const updateNotificationSettings = ({settings}) => {
return apiService.updateNotificationSettings({credentials, settings})
}
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 register = (params) => apiService.register({ credentials, 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 externalProfile = (profileUrl) => apiService.externalProfile({profileUrl, credentials})
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 backendInteractorServiceInstance = {
fetchStatus,
fetchConversation,
fetchFriends,
exportFriends,
fetchFollowers,
followUser,
unfollowUser,
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
verifyCredentials: apiService.verifyCredentials,
startFetchingTimeline,
startFetchingNotifications,
fetchMutes,
muteUser,
unmuteUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
fetchPinnedStatuses,
pinOwnStatus,
unpinOwnStatus,
tagUser,
untagUser,
addRight,
deleteRight,
deleteUser,
setActivationStatus,
register,
getCaptcha,
updateAvatar,
updateBg,
updateBanner,
updateProfile,
externalProfile,
importBlocks,
importFollows,
deleteAccount,
changePassword,
fetchSettingsMFA,
generateMfaBackupCodes,
mfaSetupOTP,
mfaConfirmOTP,
mfaDisableOTP,
fetchFollowRequests,
approveUser,
denyUser,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,
favorite,
unfavorite,
retweet,
unretweet,
updateNotificationSettings
}
return backendInteractorServiceInstance
}
export default backendInteractorService
diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js
index 35c9c23667..030e998010 100644
--- a/src/services/new_api/oauth.js
+++ b/src/services/new_api/oauth.js
@@ -1,115 +1,137 @@
-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'
+ }
+
+ 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 = `${args.instance}/oauth/authorize?${dataString}`
+ // Do the redirect...
+ const url = `${instance}/oauth/authorize?${dataString}`
- window.location.href = url
- })
+ 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)
return window.fetch(url, {
method: 'POST',
body: form
}).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`)
+ 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 oauth = {
login,
getToken,
getTokenWithCredentials,
getOrCreateApp,
verifyOTPCode,
verifyRecoveryCode
}
export default oauth
diff --git a/src/services/new_api/utils.js b/src/services/new_api/utils.js
index 078f392fc0..6696573b3b 100644
--- a/src/services/new_api/utils.js
+++ b/src/services/new_api/utils.js
@@ -1,36 +1,36 @@
const queryParams = (params) => {
return Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&')
}
const headers = (store) => {
- const accessToken = store.state.oauth.token
+ const accessToken = store.getters.getToken()
if (accessToken) {
- return {'Authorization': `Bearer ${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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 3:47 AM (18 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802370
Default Alt Text
(72 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline