Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85826527
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 2cb8b37ab6..377e82953c 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -1,314 +1,315 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
{{$t('settings.user_settings')}}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
{{ $t('settings.saving_err') }}
</div>
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
<tab-switcher>
<div :label="$t('settings.profile_tab')">
<div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<EmojiInput
type="text"
v-model="newName"
id="username"
classname="name-changer"
/>
<p>{{$t('settings.bio')}}</p>
<EmojiInput
type="textarea"
v-model="newBio"
classname="bio"
/>
<p>
<input type="checkbox" v-model="newLocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p>
<div>
<label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray">
<scope-selector
:showAll="true"
:userDefault="newDefaultScope"
+ :initialScope="newDefaultScope"
:onScopeChange="changeVis"/>
</div>
</div>
<p>
<input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="hideFollows" id="account-hide-follows">
<label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
<label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
</p>
<p>
<input type="checkbox" v-model="showRole" id="account-show-role">
<label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
<label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
</p>
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="current-avatar" />
<p>{{$t('settings.set_new_avatar')}}</p>
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner" />
<p>{{$t('settings.set_new_profile_banner')}}</p>
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview" />
<div>
<input type="file" @change="uploadFile('banner', $event)" />
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
<div class='alert error' v-if="bannerUploadError">
Error: {{ bannerUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('banner')"></i>
</div>
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview" />
<div>
<input type="file" @change="uploadFile('background', $event)" />
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
<div class='alert error' v-if="backgroundUploadError">
Error: {{ backgroundUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('background')"></i>
</div>
</div>
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
<input type="password" v-model="changePasswordInputs[0]">
</div>
<div>
<p>{{$t('settings.new_password')}}</p>
<input type="password" v-model="changePasswordInputs[1]">
</div>
<div>
<p>{{$t('settings.confirm_new_password')}}</p>
<input type="password" v-model="changePasswordInputs[2]">
</div>
<button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
<p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{$t('settings.app_name')}}</th>
<th>{{$t('settings.valid_until')}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.appName}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">
{{$t('settings.revoke_token')}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
<p>{{$t('settings.delete_account_instructions')}}</p>
<p>{{$t('login.password')}}</p>
<input type="password" v-model="deleteAccountConfirmPasswordInput">
<button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
</div>
<p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
<p v-if="deleteAccountError">{{deleteAccountError}}</p>
<button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
</div>
</div>
<div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
<div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" />
</div>
<div class="setting-item">
<h2>{{$t('settings.follow_export')}}</h2>
<Exporter :getContent="getFollowsContent" filename="friends.csv" :exportButtonLabel="$t('settings.follow_export_button')" />
</div>
<div class="setting-item">
<h2>{{$t('settings.block_import')}}</h2>
<p>{{$t('settings.import_blocks_from_a_csv_file')}}</p>
<Importer :submitHandler="importBlocks" :successMessage="$t('settings.blocks_imported')" :errorMessage="$t('settings.block_import_error')" />
</div>
<div class="setting-item">
<h2>{{$t('settings.block_export')}}</h2>
<Exporter :getContent="getBlocksContent" filename="blocks.csv" :exportButtonLabel="$t('settings.block_export_button')" />
</div>
</div>
<div :label="$t('settings.blocks_tab')">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')">
<BlockCard slot-scope="row" :userId="row.item"/>
</Autosuggest>
</div>
<BlockList :refresh="true" :getKey="identity">
<template slot="header" slot-scope="{selected}">
<div class="profile-edit-bulk-actions">
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)">
{{ $t('user_card.block') }}
<template slot="progress">{{ $t('user_card.block_progress') }}</template>
</ProgressButton>
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)">
{{ $t('user_card.unblock') }}
<template slot="progress">{{ $t('user_card.unblock_progress') }}</template>
</ProgressButton>
</div>
</template>
<template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template>
<template slot="empty">{{$t('settings.no_blocks')}}</template>
</BlockList>
</div>
<div :label="$t('settings.mutes_tab')">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')">
<MuteCard slot-scope="row" :userId="row.item"/>
</Autosuggest>
</div>
<MuteList :refresh="true" :getKey="identity">
<template slot="header" slot-scope="{selected}">
<div class="profile-edit-bulk-actions">
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)">
{{ $t('user_card.mute') }}
<template slot="progress">{{ $t('user_card.mute_progress') }}</template>
</ProgressButton>
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)">
{{ $t('user_card.unmute') }}
<template slot="progress">{{ $t('user_card.unmute_progress') }}</template>
</ProgressButton>
</div>
</template>
<template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template>
<template slot="empty">{{$t('settings.no_mutes')}}</template>
</MuteList>
</div>
</tab-switcher>
</div>
</div>
</template>
<script src="./user_settings.js">
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
}
.visibility-tray {
padding-top: 5px;
}
input[type=file] {
padding: 5px;
height: auto;
}
.banner {
max-width: 100%;
}
.uploading {
font-size: 1.5em;
margin: 0.25em;
}
.name-changer {
width: 100%;
}
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
&-usersearch-wrapper {
padding: 1em;
}
&-bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
button {
width: 10em;
}
}
}
</style>
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index c67eccf10a..115c32962f 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,773 +1,773 @@
/* eslint-env browser */
-const LOGIN_URL = '/api/account/verify_credentials.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 MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
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 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, {
method: 'POST',
body: form
})
}
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(LOGIN_URL, {
- method: 'POST',
+ return fetch(MASTODON_LOGIN_URL, {
headers: authHeaders(user)
})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {
error: response
}
}
})
+
.then((data) => data.error ? data : parseUser(data))
}
const favorite = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_FAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unfavorite = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNFAVORITE_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const retweet = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const unretweet = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNRETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
const 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 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,
fetchFollowRequests,
approveUser,
denyUser,
suggestions,
markNotificationsAsSeen,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser
}
export default apiService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 46ca760264..f14c483cc2 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,326 +1,345 @@
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
return 'status'
}
if (status.retweeted_status) {
return 'retweet'
}
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
return 'favorite'
}
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
return 'deletion'
}
if (status.text.match(/started following/) || status.activity_type === 'follow') {
return 'follow'
}
return 'unknown'
}
export const parseUser = (data) => {
const output = {}
const masto = data.hasOwnProperty('acct')
// case for users in "mentions" property for statuses in MastoAPI
const mastoShort = masto && !data.hasOwnProperty('avatar')
output.id = String(data.id)
if (masto) {
output.screen_name = data.acct
// There's nothing else to get
if (mastoShort) {
return output
}
output.name = data.display_name
output.name_html = addEmojis(data.display_name, data.emojis)
// output.description = ??? missing
output.description_html = addEmojis(data.note, data.emojis)
// Utilize avatar_static for gif avatars?
output.profile_image_url = data.avatar
output.profile_image_url_original = data.avatar
// Same, utilize header_static?
output.cover_photo = data.header
output.friends_count = data.following_count
output.bot = data.bot
output.statusnet_profile_url = data.url
if (data.pleroma) {
const relationship = data.pleroma.relationship
if (relationship) {
output.follows_you = relationship.followed_by
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
}
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) {
+ if (data.source.pleroma.show_role) {
+ console.log(data.source)
+ console.log(data.pleroma)
+ }
+ output.no_rich_text = data.source.pleroma.no_rich_text
+ output.show_role = data.source.pleroma.show_role
+ }
}
// TODO: handle is_local
output.is_local = !output.screen_name.includes('@')
} else {
output.screen_name = data.screen_name
output.name = data.name
output.name_html = data.name_html
output.description = data.description
output.description_html = data.description_html
output.profile_image_url = data.profile_image_url
output.profile_image_url_original = data.profile_image_url_original
output.cover_photo = data.cover_photo
output.friends_count = data.friends_count
// output.bot = ??? missing
output.statusnet_profile_url = data.statusnet_profile_url
output.statusnet_blocking = data.statusnet_blocking
output.is_local = data.is_local
output.role = data.role
output.show_role = data.show_role
output.follows_you = data.follows_you
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,
admin: data.rights.admin
}
}
output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope
output.hide_follows = data.hide_follows
output.hide_followers = data.hide_followers
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket token
output.token = data.token
}
output.created_at = new Date(data.created_at)
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
output.friendIds = []
output.followerIds = []
output.pinnedStatuseIds = []
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.tags = output.tags || []
output.rights = output.rights || {}
return output
}
export const parseAttachment = (data) => {
const output = {}
const masto = !data.hasOwnProperty('oembed')
if (masto) {
// Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta // not present in BE yet
output.id = data.id
} else {
output.mimetype = data.mimetype
// output.meta = ??? missing
}
output.url = data.url
output.description = data.description
return output
}
export const addEmojis = (string, emojis) => {
return emojis.reduce((acc, emoji) => {
return acc.replace(
new RegExp(`:${emoji.shortcode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
)
}, string)
}
export const parseStatus = (data) => {
const output = {}
const masto = data.hasOwnProperty('account')
if (masto) {
output.favorited = data.favourited
output.fave_num = data.favourites_count
output.repeated = data.reblogged
output.repeat_num = data.reblogs_count
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
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
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
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
} else {
output.text = data.content
output.summary = data.spoiler_text
}
output.in_reply_to_status_id = data.in_reply_to_id
output.in_reply_to_user_id = data.in_reply_to_account_id
output.replies_count = data.replies_count
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.reblog)
}
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
output.external_url = data.url
output.pinned = data.pinned
} else {
output.favorited = data.favorited
output.fave_num = data.fave_num
output.repeated = data.repeated
output.repeat_num = data.repeat_num
// catchall, temporary
// Object.assign(output, data)
output.type = qvitterStatusType(data)
if (data.nsfw === undefined) {
output.nsfw = isNsfw(data)
if (data.retweeted_status) {
output.nsfw = data.retweeted_status.nsfw
}
} else {
output.nsfw = data.nsfw
}
output.statusnet_html = data.statusnet_html
output.text = data.text
output.in_reply_to_status_id = data.in_reply_to_status_id
output.in_reply_to_user_id = data.in_reply_to_user_id
output.in_reply_to_screen_name = data.in_reply_to_screen_name
output.statusnet_conversation_id = data.statusnet_conversation_id
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.retweeted_status)
}
output.summary = data.summary
output.summary_html = data.summary_html
output.external_url = data.external_url
output.is_local = data.is_local
}
output.id = String(data.id)
output.visibility = data.visibility
output.card = data.card
output.created_at = new Date(data.created_at)
// Converting to string, the right way.
output.in_reply_to_status_id = output.in_reply_to_status_id
? String(output.in_reply_to_status_id)
: null
output.in_reply_to_user_id = output.in_reply_to_user_id
? String(output.in_reply_to_user_id)
: null
output.user = parseUser(masto ? data.account : data.user)
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
.map(parseAttachment)
const retweetedStatus = masto ? data.reblog : data.retweeted_status
if (retweetedStatus) {
output.retweeted_status = parseStatus(retweetedStatus)
}
output.favoritedBy = []
output.rebloggedBy = []
return output
}
export const parseNotification = (data) => {
const mastoDict = {
'favourite': 'like',
'reblog': 'repeat'
}
const masto = !data.hasOwnProperty('ntype')
const output = {}
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = output.type === 'follow'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)
output.type = data.ntype
output.seen = Boolean(data.is_seen)
output.status = output.type === 'like'
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
output.from_profile = parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
output.id = parseInt(data.id)
return output
}
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 7:58 AM (20 h, 42 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799835
Default Alt Text
(45 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline