diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/api/api.service.js | 52 | ||||
| -rw-r--r-- | src/services/entity_normalizer/entity_normalizer.service.js | 263 | ||||
| -rw-r--r-- | src/services/notification_utils/notification_utils.js | 4 | ||||
| -rw-r--r-- | src/services/status_poster/status_poster.service.js | 1 |
4 files changed, 314 insertions, 6 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 4ee95bd1..2b1ef5df 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -41,7 +41,10 @@ const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' +const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' + import { each, map } from 'lodash' +import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' import 'whatwg-fetch' const oldfetch = window.fetch @@ -70,6 +73,7 @@ const updateAvatar = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -87,6 +91,7 @@ const updateBg = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -110,6 +115,7 @@ const updateBanner = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -237,24 +243,28 @@ const fetchUser = ({id, credentials}) => { let url = `${USER_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => parseUser(data)) } const fetchFriends = ({id, credentials}) => { let url = `${FRIENDS_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchFollowers = ({id, credentials}) => { let url = `${FOLLOWERS_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchAllFollowing = ({username, credentials}) => { const url = `${ALL_FOLLOWING_URL}/${username}.json` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchFollowRequests = ({credentials}) => { @@ -266,13 +276,27 @@ const fetchFollowRequests = ({credentials}) => { const fetchConversation = ({id, credentials}) => { let url = `${CONVERSATION_URL}/${id}.json?count=100` 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(parseStatus)) } const fetchStatus = ({id, credentials}) => { let url = `${STATUS_URL}/${id}.json` 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 setUserMute = ({id, credentials, muted = true}) => { @@ -300,13 +324,14 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use notifications: QVITTER_USER_NOTIFICATIONS_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, user: QVITTER_USER_TIMELINE_URL, + favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: TAG_TIMELINE_URL } + const isNotifications = timeline === 'notifications' + const params = [] let url = timelineUrls[timeline] - let params = [] - if (since) { params.push(['since_id', since]) } @@ -330,9 +355,10 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use if (data.ok) { return data } - throw new Error('Error fetching timeline') + throw new Error('Error fetching timeline', data) }) .then((data) => data.json()) + .then((data) => data.map(isNotifications ? parseNotification : parseStatus)) } const verifyCredentials = (user) => { @@ -340,6 +366,16 @@ const verifyCredentials = (user) => { method: 'POST', 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 }) => { @@ -391,6 +427,16 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me 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 }) => { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js new file mode 100644 index 00000000..6fc6d152 --- /dev/null +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -0,0 +1,263 @@ +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 = null // missing + output.name_html = data.display_name + + output.description = null // missing + output.description_html = data.note + + // 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 pleroma = data.pleroma + output.follows_you = pleroma.follows_you + output.statusnet_blocking = pleroma.statusnet_blocking + output.muted = pleroma.muted + } + + // Missing, trying to recover + 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 = null // missing + + output.statusnet_profile_url = data.statusnet_profile_url + + output.statusnet_blocking = data.statusnet_blocking + + output.is_local = data.is_local + + output.follows_you = data.follows_you + + output.muted = data.muted + + // QVITTER ONLY FOR NOW + // Really only applies to logged in user, really.. I THINK + output.rights = data.rights + output.no_rich_text = data.no_rich_text + output.default_scope = data.default_scope + output.hide_network = data.hide_network + output.background_image = data.background_image + } + + output.created_at = new Date(data.created_at) + output.locked = data.locked + output.followers_count = data.followers_count + output.statuses_count = data.statuses_count + + return output +} + +const parseAttachment = (data) => { + const output = {} + const masto = !data.hasOwnProperty('oembed') + + if (masto) { + // Not exactly same... + output.mimetype = data.type + output.meta = data.meta // not present in BE yet + } else { + output.mimetype = data.mimetype + output.meta = null // missing + } + + output.url = data.url + output.description = data.description + + return output +} + +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 = data.content + + // Not exactly the same but works? + output.text = data.content + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_account_id + + // Missing!! fix in UI? + output.in_reply_to_screen_name = null + + // Not exactly the same but works + output.statusnet_conversation_id = data.id + + if (output.type === 'retweet') { + output.retweeted_status = parseStatus(data.reblog) + } + + output.summary = data.spoiler_text + output.external_url = data.url + + // FIXME missing!! + output.is_local = false + } 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.external_url = data.external_url + output.is_local = data.is_local + } + + output.id = String(data.id) + output.visibility = data.visibility + output.created_at = new Date(data.created_at) + + 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) + } + + 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 = null // missing + output.status = parseStatus(data.status) + output.action = output.status // not sure + 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 = String(data.id) + + return output +} + +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) +} diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index f5ac0d47..c3879677 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -10,8 +10,8 @@ export const visibleTypes = store => ([ ].filter(_ => _)) export const visibleNotificationsFromStore = store => { - // Don't know why, but sortBy([seen, -action.id]) doesn't work. - let sortedNotifications = sortBy(notificationsFromStore(store), ({action}) => -action.id) + // map is just to clone the array since sort mutates it and it causes some issues + let sortedNotifications = notificationsFromStore(store).map(_ => _).sort((a, b) => a.action.id > b.action.id ? -1 : 1) sortedNotifications = sortBy(sortedNotifications, 'seen') return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type)) } diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index 1e20d336..f1932bb6 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -5,7 +5,6 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = const mediaIds = map(media, 'id') return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks}) - .then((data) => data.json()) .then((data) => { if (!data.error) { store.dispatch('addNewStatuses', { |
