From 319bb4ac2895b8eb62da42e3f95addc9bb67b1a0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 24 Nov 2019 18:50:28 +0200 Subject: initial streaming work --- src/services/api/api.service.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'src/services/api/api.service.js') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8f5eb416..7f27564f 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -71,6 +71,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute` const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' +const MASTODON_STREAMING = '/api/v1/streaming' const oldfetch = window.fetch @@ -932,6 +933,45 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => { }) } +export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { + return Object.entries({ + ...(credentials + ? { access_token: credentials } + : {} + ), + stream, + ...args + }).reduce((acc, [key, val]) => { + return acc + `${key}=${val}&` + }, MASTODON_STREAMING + '?') +} + +const MASTODON_STREAMING_EVENTS = new Set([ + 'update', + 'notification', + 'delete', + 'filters_changed' +]) + +export const handleMastoWS = (wsEvent) => { + console.debug('Event', wsEvent) + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if (MASTODON_STREAMING_EVENTS.has(event)) { + const data = payload ? JSON.parse(payload) : null + if (event === 'update') { + return { event, status: parseStatus(data) } + } else if (event === 'notification') { + return { event, notification: parseNotification(data) } + } + } else { + console.warn('Unknown event', wsEvent) + return null + } +} + const apiService = { verifyCredentials, fetchTimeline, -- cgit v1.2.3-70-g09d2 From 505fb260610e557e27bbc5d27515337ea07e0e3e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 8 Dec 2019 19:18:38 +0200 Subject: better wrapper for websocket --- src/modules/api.js | 43 ++++++++++---------- src/services/api/api.service.js | 46 +++++++++++++++++++++- .../backend_interactor_service.js | 15 ++----- 3 files changed, 69 insertions(+), 35 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/modules/api.js b/src/modules/api.js index 185c9db6..593f8498 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -34,36 +34,35 @@ const api = { // MastoAPI 'User' sockets startMastoUserSocket (store) { const { state, dispatch } = store - state.mastoUserSocket = state.backendInteractor - .startUserSocket({ - store, - onMessage: (message) => { - if (!message) return // pings - if (message.event === 'notification') { - dispatch('addNewNotifications', { - notifications: [message.notification], - older: false - }) - } else if (message.event === 'update') { - dispatch('addNewStatuses', { - statuses: [message.status], - userId: false, - showImmediately: false, - timeline: 'friends' - }) - } + state.mastoUserSocket = state.backendInteractor.startUserSocket({ store }) + state.mastoUserSocket.addEventListener( + 'message', + ({ detail: message }) => { + if (!message) return // pings + if (message.event === 'notification') { + dispatch('addNewNotifications', { + notifications: [message.notification], + older: false + }) + } else if (message.event === 'update') { + dispatch('addNewStatuses', { + statuses: [message.status], + userId: false, + showImmediately: false, + timeline: 'friends' + }) } - }) - state.mastoUserSocket.addEventListener('error', error => { + } + ) + state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) }) - state.mastoUserSocket.addEventListener('close', closeEvent => { + state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { const ignoreCodes = new Set([ 1000, // Normal (intended) closure 1001 // Going away ]) const { code } = closeEvent - console.debug('Socket closure event:', closeEvent) if (ignoreCodes.has(code)) { console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) } else { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 7f27564f..c6818df4 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -953,8 +953,52 @@ const MASTODON_STREAMING_EVENTS = new Set([ 'filters_changed' ]) +// A thin wrapper around WebSocket API that allows adding a pre-processor to it +// Uses EventTarget and a CustomEvent to proxy events +export const ProcessedWS = ({ + url, + preprocessor = handleMastoWS, + id = 'Unknown' +}) => { + const eventTarget = new EventTarget() + const socket = new WebSocket(url) + if (!socket) throw new Error(`Failed to create socket ${id}`) + const proxy = (original, eventName, processor = a => a) => { + original.addEventListener(eventName, (eventData) => { + eventTarget.dispatchEvent(new CustomEvent( + eventName, + { detail: processor(eventData) } + )) + }) + } + socket.addEventListener('open', (wsEvent) => { + console.debug(`[WS][${id}] Socket connected`, wsEvent) + }) + socket.addEventListener('error', (wsEvent) => { + console.debug(`[WS][${id}] Socket errored`, wsEvent) + }) + socket.addEventListener('close', (wsEvent) => { + console.debug( + `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, + wsEvent + ) + }) + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', preprocessor) + proxy(socket, 'error') + + return eventTarget +} + export const handleMastoWS = (wsEvent) => { - console.debug('Event', wsEvent) const { data } = wsEvent if (!data) return const parsedEvent = JSON.parse(data) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 33b79a40..b7372ed0 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,4 +1,4 @@ -import apiService, { getMastodonSocketURI, handleMastoWS } from '../api/api.service.js' +import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' @@ -20,19 +20,10 @@ const backendInteractorService = credentials => ({ return followRequestFetcher.startFetching({ store, credentials }) }, - startUserSocket ({ store, onMessage }) { + startUserSocket ({ store }) { const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) - const socket = new WebSocket(url) - console.debug('Socket created:', socket) - if (socket) { - socket.addEventListener('open', (wsEvent) => console.debug('MastoAPI User WebSocket connection established')) - socket.addEventListener('message', (wsEvent) => onMessage(handleMastoWS(wsEvent))) - socket.addEventListener('error', (error) => console.error('MastoApi User WebSocket Error:', error)) - return socket - } else { - throw new Error('failed to connect to socket') - } + return ProcessedWS({ url, id: 'User' }) }, ...Object.entries(apiService).reduce((acc, [key, func]) => { -- cgit v1.2.3-70-g09d2 From 6acd889589e46b18491d96b5fa992154b4e58d88 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 10 Dec 2019 21:30:27 +0200 Subject: Option to enable streaming --- src/components/settings/settings.js | 14 +++++++++++++- src/components/settings/settings.vue | 9 +++++++++ src/i18n/en.json | 2 ++ src/i18n/ru.json | 2 ++ src/modules/api.js | 18 ++++++++++++++++++ src/modules/config.js | 1 + src/modules/users.js | 14 +++++++++++--- src/services/api/api.service.js | 6 ++++++ 8 files changed, 62 insertions(+), 4 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index c49083f9..2d7723cc 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -84,7 +84,7 @@ const settings = { } }]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Special cases (need to transform values) + // Special cases (need to transform values or perform actions first) muteWordsString: { get () { return this.$store.getters.mergedConfig.muteWords.join('\n') }, set (value) { @@ -93,6 +93,18 @@ const settings = { value: filter(value.split('\n'), (word) => trim(word).length > 0) }) } + }, + useStreamingApi: { + get () { return this.$store.getters.mergedConfig.useStreamingApi }, + set (value) { + const promise = value + ? this.$store.dispatch('enableMastoSockets') + : this.$store.dispatch('disableMastoSockets') + + promise.then(() => { + this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) + }) + } } }, // Updating nested properties diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index c4021137..b40c85dd 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -73,6 +73,15 @@ +
  • + + {{ $t('settings.useStreamingApi') }} +
    + + {{ $t('settings.useStreamingApiWarning') }} + +
    +
  • {{ $t('settings.autoload') }} diff --git a/src/i18n/en.json b/src/i18n/en.json index 85146ef5..60fc792f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -358,6 +358,8 @@ "post_status_content_type": "Post status content type", "stop_gifs": "Play-on-hover GIFs", "streaming": "Enable automatic streaming of new posts when scrolled to the top", + "useStreamingApi": "Receive posts and notifications real-time", + "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)", "text": "Text", "theme": "Theme", "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 19e10f1e..4cb2d497 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -219,6 +219,8 @@ "subject_input_always_show": "Всегда показывать поле ввода темы", "stop_gifs": "Проигрывать GIF анимации только при наведении", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", + "useStreamingApi": "Получать сообщения и уведомления в реальном времени", + "useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)", "text": "Текст", "theme": "Тема", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", diff --git a/src/modules/api.js b/src/modules/api.js index 593f8498..dc91d00e 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -31,6 +31,18 @@ const api = { } }, actions: { + // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets + enableMastoSockets (store) { + const { state, dispatch } = store + if (state.mastoUserSocket) return + dispatch('startMastoUserSocket') + }, + disableMastoSockets (store) { + const { state, dispatch } = store + if (!state.mastoUserSocket) return + dispatch('stopMastoUserSocket') + }, + // MastoAPI 'User' sockets startMastoUserSocket (store) { const { state, dispatch } = store @@ -81,6 +93,12 @@ const api = { dispatch('stopFetchingNotifications') }) }, + stopMastoUserSocket ({ state, dispatch }) { + dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingNotifications') + console.log(state.mastoUserSocket) + state.mastoUserSocket.close() + }, // Timelines startFetchingTimeline (store, { diff --git a/src/modules/config.js b/src/modules/config.js index 329b4091..74025db1 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -35,6 +35,7 @@ export const defaultState = { highlight: {}, interfaceLanguage: browserLocale, hideScopeNotice: false, + useStreamingApi: false, scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default diff --git a/src/modules/users.js b/src/modules/users.js index 6bdaf193..cbec6063 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -469,14 +469,22 @@ const users = { store.dispatch('initializeSocket') } - store.dispatch('startMastoUserSocket').catch((error) => { - console.error('Failed initializing MastoAPI Streaming socket', error) + const startPolling = () => { // Start getting fresh posts. store.dispatch('startFetchingTimeline', { timeline: 'friends' }) // Start fetching notifications store.dispatch('startFetchingNotifications') - }) + } + + if (store.getters.mergedConfig.useStreamingApi) { + store.dispatch('enableMastoSockets').catch((error) => { + console.error('Failed initializing MastoAPI Streaming socket', error) + startPolling() + }) + } else { + startPolling() + } // Get user mutes store.dispatch('fetchMutes') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index c6818df4..517b953e 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -983,18 +983,24 @@ export const ProcessedWS = ({ wsEvent ) }) + // Commented code reason: very spammy, uncomment to enable message debug logging + /* socket.addEventListener('message', (wsEvent) => { console.debug( `[WS][${id}] Message received`, wsEvent ) }) + /**/ proxy(socket, 'open') proxy(socket, 'close') proxy(socket, 'message', preprocessor) proxy(socket, 'error') + // 1000 = Normal Closure + eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } + return eventTarget } -- cgit v1.2.3-70-g09d2 From 63a5f50e7c4acfc7676a1093990d0377dcb1a39f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 11 Dec 2019 18:20:43 +0200 Subject: fix deletes causing errors --- src/services/api/api.service.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/services/api/api.service.js') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 517b953e..5f706dc0 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1010,6 +1010,10 @@ export const handleMastoWS = (wsEvent) => { const parsedEvent = JSON.parse(data) const { event, payload } = parsedEvent if (MASTODON_STREAMING_EVENTS.has(event)) { + // MastoBE and PleromaBE both send payload for delete as a PLAIN string + if (event === 'delete') { + return { event, id: payload } + } const data = payload ? JSON.parse(payload) : null if (event === 'update') { return { event, status: parseStatus(data) } -- cgit v1.2.3-70-g09d2