From d7bd294666cba08b6f6a8d447fbdf4cd59e66b2b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 15 Jan 2019 18:39:24 +0300 Subject: migrated some tests to normalizer, fixed some potential bug, fixed tests to use normalized naming instead of raw qvitter api objects. needs more tests tho. --- .../entity_normalizer/entity_normalizer.service.js | 212 +++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/services/entity_normalizer/entity_normalizer.service.js (limited to 'src/services/entity_normalizer/entity_normalizer.service.js') 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..adc7f047 --- /dev/null +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -0,0 +1,212 @@ +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 = 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 + + // 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.is_local = data.is_local + } + + 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) => { + // TODO A little bit messy ATM but works with both APIs + return { + ...data, + mimetype: data.mimetype || data.type + } +} + +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_user_id + + // Not exactly the same but works + output.statusnet_conversation_id = data.id + } 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_id + output.in_reply_to_user_id = data.in_reply_to_account_id + + output.statusnet_conversation_id = data.statusnet_conversation_id + } + + output.id = Number(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(_ => ({ + id: _.id, + following: _.following // FIXME: MastoAPI doesn't have this + })) + + 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 = null // missing + output.from_profile = parseUser(data.account) + } else { + const parsedNotice = parseStatus(data.notice) + output.type = data.ntype + output.seen = 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 = data.id + + return output +} + +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) +} -- cgit v1.2.3-70-g09d2 From 9682ee66ce82505fce9a11056bcda2af9b8c4e5a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 17 Jan 2019 19:23:14 +0300 Subject: added conversions to ids for consistency from the get-go --- src/services/entity_normalizer/entity_normalizer.service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/services/entity_normalizer/entity_normalizer.service.js') diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index adc7f047..2c8f5b54 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -29,7 +29,7 @@ export const parseUser = (data) => { // case for users in "mentions" property for statuses in MastoAPI const mastoShort = masto && !data.hasOwnProperty('avatar') - output.id = data.id + output.id = String(data.id) if (masto) { output.screen_name = data.acct @@ -152,7 +152,7 @@ export const parseStatus = (data) => { output.statusnet_conversation_id = data.statusnet_conversation_id } - output.id = Number(data.id) + output.id = String(data.id) output.visibility = data.visibility output.created_at = new Date(data.created_at) @@ -201,7 +201,7 @@ export const parseNotification = (data) => { } output.created_at = new Date(data.created_at) - output.id = data.id + output.id = String(data.id) return output } -- cgit v1.2.3-70-g09d2 From 1e61c8140b3921183f7721ec3e0db00e671a4410 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 17 Jan 2019 20:44:37 +0300 Subject: tests for the tests god! bugfixes for bugfixes throne! --- .../entity_normalizer/entity_normalizer.service.js | 12 +- .../entity_normalizer/entity_normalizer.spec.js | 209 ++++++++++++++++++--- 2 files changed, 198 insertions(+), 23 deletions(-) (limited to 'src/services/entity_normalizer/entity_normalizer.service.js') diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 2c8f5b54..ca0f36db 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -122,6 +122,10 @@ export const parseStatus = (data) => { // Not exactly the same but works output.statusnet_conversation_id = data.id + + if (output.type === 'retweet') { + output.retweeted_status = parseStatus(data.reblog) + } } else { output.favorited = data.favorited output.fave_num = data.fave_num @@ -150,6 +154,10 @@ export const parseStatus = (data) => { output.in_reply_to_user_id = data.in_reply_to_account_id output.statusnet_conversation_id = data.statusnet_conversation_id + + if (output.type === 'retweet') { + output.retweeted_status = parseStatus(data.retweeted_status) + } } output.id = String(data.id) @@ -187,12 +195,12 @@ export const parseNotification = (data) => { output.type = mastoDict[data.type] || data.type output.seen = null // missing output.status = parseStatus(data.status) - output.action = null // missing + output.action = output.status // not sure output.from_profile = parseUser(data.account) } else { const parsedNotice = parseStatus(data.notice) output.type = data.ntype - output.seen = data.is_seen + output.seen = Boolean(data.is_seen) output.status = output.type === 'like' ? parseStatus(data.notice.favorited_status) : parsedNotice diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 2eddd470..bc127f79 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,6 @@ import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import mastoapidata from '../../../../fixtures/mastoapi.json' +import qvitterapidata from '../../../../fixtures/statuses.json' const makeMockStatusQvitter = (overrides = {}) => { return Object.assign({ @@ -64,39 +66,204 @@ const makeMockUserQvitter = (overrides = {}) => { }, overrides) } +const makeMockUserMasto = (overrides = {}) => { + return Object.assign({ + acct: 'hj', + avatar: + 'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png', + avatar_static: + 'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png', + bot: false, + created_at: '2017-12-17T21:54:14.000Z', + display_name: 'whatever whatever whatever witch', + emojis: [], + fields: [], + followers_count: 705, + following_count: 326, + header: + 'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png', + header_static: + 'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png', + id: '1', + locked: false, + note: + 'Volatile Internet Weirdo. Name pronounced as Hee Jay. JS and Java dark arts mage, Elixir trainee. I love sampo and lain. Matrix is @hj:matrix.heldscal.la Pronouns are whatever. Do not DM me unless it\'s truly private matter and you\'re instance\'s admin or you risk your DM to be reposted publicly.Wish i was Finnish girl.', + pleroma: { confirmation_pending: false, tags: null }, + source: { note: '', privacy: 'public', sensitive: false }, + statuses_count: 41775, + url: 'https://shigusegubu.club/users/hj', + username: 'hj' + }, overrides) +} + +const makeMockStatusMasto = (overrides = {}) => { + return Object.assign({ + account: makeMockUserMasto(), + application: { name: 'Web', website: null }, + content: + '@sampo god i wish i was there', + created_at: '2019-01-17T16:29:23.000Z', + emojis: [], + favourited: false, + favourites_count: 1, + id: '10423476', + in_reply_to_account_id: '14660', + in_reply_to_id: '10423197', + language: null, + media_attachments: [], + mentions: [ + { + acct: 'sampo@pleroma.soykaf.com', + id: '14660', + url: 'https://pleroma.soykaf.com/users/sampo', + username: 'sampo' + } + ], + muted: false, + reblog: null, + reblogged: false, + reblogs_count: 0, + replies_count: 0, + sensitive: false, + spoiler_text: '', + tags: [], + uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639', + url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639', + visibility: 'public' + }, overrides) +} + +const makeMockNotificationQvitter = (overrides = {}) => { + return Object.assign({ + notice: makeMockStatusQvitter(), + ntype: 'follow', + from_profile: makeMockUserQvitter(), + is_seen: 0, + id: 123 + }, overrides) +} + parseNotification parseUser parseStatus makeMockStatusQvitter makeMockUserQvitter -describe('QVitter preprocessing', () => { - it('identifies favorites', () => { - const fav = { - uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00', - is_post_verb: false - } +describe.only('API Entities normalizer', () => { + describe('statuses', () => { + describe('QVitter preprocessing', () => { + it('doesn\'t blow up', () => { + const parsed = qvitterapidata.map(parseStatus) + expect(parsed.length).to.eq(qvitterapidata.length) + }) - const mastoFav = { - uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite', - is_post_verb: false - } + it('identifies favorites', () => { + const fav = { + uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00', + is_post_verb: false + } - expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite') - expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite') - }) + const mastoFav = { + uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite', + is_post_verb: false + } + + expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite') + expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite') + }) + + it('processes repeats correctly', () => { + const post = makeMockStatusQvitter({ retweeted_status: null, id: 'deadbeef' }) + const repeat = makeMockStatusQvitter({ retweeted_status: post, is_post_verb: false, id: 'foobar' }) + + const parsedPost = parseStatus(post) + const parsedRepeat = parseStatus(repeat) + + expect(parsedPost).to.have.property('type', 'status') + expect(parsedRepeat).to.have.property('type', 'retweet') + expect(parsedRepeat).to.have.property('retweeted_status') + expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') + }) + + it('sets nsfw for statuses with the #nsfw tag', () => { + const safe = makeMockStatusQvitter({id: '1', text: 'Hello oniichan'}) + const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw'}) - it('sets nsfw for statuses with the #nsfw tag', () => { - const safe = makeMockStatusQvitter({id: '1', text: 'Hello oniichan'}) - const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw'}) + expect(parseStatus(safe).nsfw).to.eq(false) + expect(parseStatus(nsfw).nsfw).to.eq(true) + }) + + it('leaves existing nsfw settings alone', () => { + const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw', nsfw: false}) + + expect(parseStatus(nsfw).nsfw).to.eq(false) + }) + }) + + describe('Mastoapi preprocessing and converting', () => { + it('doesn\'t blow up', () => { + const parsed = mastoapidata.map(parseStatus) + expect(parsed.length).to.eq(mastoapidata.length) + }) + + it('processes repeats correctly', () => { + const post = makeMockStatusMasto({ reblog: null, id: 'deadbeef' }) + const repeat = makeMockStatusMasto({ reblog: post, id: 'foobar' }) + + const parsedPost = parseStatus(post) + const parsedRepeat = parseStatus(repeat) + + expect(parsedPost).to.have.property('type', 'status') + expect(parsedRepeat).to.have.property('type', 'retweet') + expect(parsedRepeat).to.have.property('retweeted_status') + expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') + }) + }) + }) + // Statuses generally already contain some info regarding users and there's nearly 1:1 mapping, so very little to test + describe('users (MastoAPI)', () => { + it('sets correct is_local for users depending on their screen_name', () => { + const local = makeMockUserMasto({ acct: 'foo' }) + const remote = makeMockUserMasto({ acct: 'foo@bar.baz' }) - expect(parseStatus(safe).nsfw).to.eq(false) - expect(parseStatus(nsfw).nsfw).to.eq(true) + expect(parseUser(local)).to.have.property('is_local', true) + expect(parseUser(remote)).to.have.property('is_local', false) + }) }) - it('leaves existing nsfw settings alone', () => { - const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw', nsfw: false}) + // We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI + // is more of an afterthought + describe('notifications (QvitterAPI)', () => { + it('correctly normalizes data to FE\'s format', () => { + const notif = makeMockNotificationQvitter({ + id: 123, + notice: makeMockStatusQvitter({ id: 444 }), + from_profile: makeMockUserQvitter({ id: 'spurdo' }) + }) + expect(parseNotification(notif)).to.have.property('id', '123') + expect(parseNotification(notif)).to.have.property('seen', false) + expect(parseNotification(notif)).to.have.deep.property('status.id', '444') + expect(parseNotification(notif)).to.have.deep.property('action.id', '444') + expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo') + }) - expect(parseStatus(nsfw).nsfw).to.eq(false) + it('correctly normalizes favorite notifications', () => { + const notif = makeMockNotificationQvitter({ + id: 123, + ntype: 'like', + notice: makeMockStatusQvitter({ + id: 444, + favorited_status: makeMockStatusQvitter({ id: 4412 }) + }), + is_seen: 1, + from_profile: makeMockUserQvitter({ id: 'spurdo' }) + }) + expect(parseNotification(notif)).to.have.property('id', '123') + expect(parseNotification(notif)).to.have.property('type', 'like') + expect(parseNotification(notif)).to.have.property('seen', true) + expect(parseNotification(notif)).to.have.deep.property('status.id', '4412') + expect(parseNotification(notif)).to.have.deep.property('action.id', '444') + expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo') + }) }) }) -- cgit v1.2.3-70-g09d2 From 93cbb58212ebb83cee5bc89f8cef1ebb58969f5c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 17 Jan 2019 22:11:51 +0300 Subject: fix login and favorites tab... --- src/components/user_profile/user_profile.js | 13 +++++ src/components/user_profile/user_profile.vue | 2 +- src/modules/users.js | 61 +++++++++++----------- src/services/api/api.service.js | 12 +++++ .../entity_normalizer/entity_normalizer.service.js | 8 +++ 5 files changed, 64 insertions(+), 32 deletions(-) (limited to 'src/services/entity_normalizer/entity_normalizer.service.js') diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 7f17ef69..c9197a1c 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -5,13 +5,16 @@ import Timeline from '../timeline/timeline.vue' const UserProfile = { created () { this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) if (!this.user.id) { this.$store.dispatch('fetchUser', this.fetchBy) } }, destroyed () { this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') }, computed: { timeline () { @@ -26,6 +29,9 @@ const UserProfile = { userName () { return this.$route.params.name || this.user.screen_name }, + isUs () { + return this.userId === this.$store.state.users.currentUser.id + }, friends () { return this.user.friends }, @@ -65,21 +71,28 @@ const UserProfile = { } }, watch: { + // TODO get rid of this copypasta userName () { if (this.isExternal) { return } this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) }, userId () { if (!this.isExternal) { return } this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) }, user () { if (this.user.id && !this.user.followers) { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 265fc65b..e53727ff 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,7 +20,7 @@ - +