From 1e9b209f06247ed6f433afc27fb5d53e97f313e8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Jan 2024 18:10:49 -0600 Subject: [PATCH] Differentiate instance V1 and V2 Fixes https://gitlab.com/soapbox-pub/soapbox/-/issues/1635 --- src/actions/instance.ts | 38 ++++---------- src/init/soapbox-load.tsx | 4 +- src/reducers/instance.test.ts | 95 ----------------------------------- src/reducers/instance.ts | 29 +++++++---- 4 files changed, 30 insertions(+), 136 deletions(-) diff --git a/src/actions/instance.ts b/src/actions/instance.ts index c6ccd053b..6332ad198 100644 --- a/src/actions/instance.ts +++ b/src/actions/instance.ts @@ -2,7 +2,6 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import get from 'lodash/get'; import { gte } from 'semver'; -import KVStore from 'soapbox/storage/kv-store'; import { RootState } from 'soapbox/store'; import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth'; import { MASTODON, parseVersion, PLEROMA, REBASED } from 'soapbox/utils/features'; @@ -20,15 +19,6 @@ export const getHost = (state: RootState) => { } }; -export const rememberInstance = createAsyncThunk( - 'instance/remember', - async(host: string) => { - const instance = await KVStore.getItemOrError(`instance:${host}`); - - return { instance, host }; - }, -); - const supportsInstanceV2 = (instance: Record): boolean => { const v = parseVersion(get(instance, 'version')); return (v.software === MASTODON && gte(v.compatVersion, '4.0.0')) || @@ -41,14 +31,19 @@ const needsNodeinfo = (instance: Record): boolean => { return v.software === PLEROMA && !get(instance, ['pleroma', 'metadata']); }; -export const fetchInstance = createAsyncThunk<{ instance: Record; host?: string | null }, string | null | undefined, { state: RootState }>( +interface InstanceData { + instance: Record; + host: string | null | undefined; +} + +export const fetchInstance = createAsyncThunk( 'instance/fetch', async(host, { dispatch, getState, rejectWithValue }) => { try { const { data: instance } = await api(getState).get('/api/v1/instance'); if (supportsInstanceV2(instance)) { - return dispatch(fetchInstanceV2(host)) as any as { instance: Record; host?: string | null }; + dispatch(fetchInstanceV2(host)); } if (needsNodeinfo(instance)) { @@ -61,8 +56,8 @@ export const fetchInstance = createAsyncThunk<{ instance: Record; h }, ); -export const fetchInstanceV2 = createAsyncThunk<{ instance: Record; host?: string | null }, string | null | undefined, { state: RootState }>( - 'instance/fetch', +export const fetchInstanceV2 = createAsyncThunk( + 'instanceV2/fetch', async(host, { getState, rejectWithValue }) => { try { const { data: instance } = await api(getState).get('/api/v2/instance'); @@ -73,21 +68,6 @@ export const fetchInstanceV2 = createAsyncThunk<{ instance: Record; }, ); -/** Tries to remember the instance from browser storage before fetching it */ -export const loadInstance = createAsyncThunk( - 'instance/load', - async(_arg, { dispatch, getState }) => { - const host = getHost(getState()); - const rememberedInstance = await dispatch(rememberInstance(host || '')); - - if (rememberedInstance.payload && supportsInstanceV2((rememberedInstance.payload as any).instance)) { - await dispatch(fetchInstanceV2(host)); - } else { - await dispatch(fetchInstance(host)); - } - }, -); - export const fetchNodeinfo = createAsyncThunk( 'nodeinfo/fetch', async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'), diff --git a/src/init/soapbox-load.tsx b/src/init/soapbox-load.tsx index 4c88f2a7e..cc9b883f1 100644 --- a/src/init/soapbox-load.tsx +++ b/src/init/soapbox-load.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { IntlProvider } from 'react-intl'; -import { loadInstance } from 'soapbox/actions/instance'; +import { fetchInstance } from 'soapbox/actions/instance'; import { fetchMe } from 'soapbox/actions/me'; import { loadSoapboxConfig } from 'soapbox/actions/soapbox'; import LoadingScreen from 'soapbox/components/loading-screen'; @@ -20,7 +20,7 @@ const loadInitial = () => { // Await for authenticated fetch await dispatch(fetchMe()); // Await for feature detection - await dispatch(loadInstance()); + await dispatch(fetchInstance()); // Await for configuration await dispatch(loadSoapboxConfig()); }; diff --git a/src/reducers/instance.test.ts b/src/reducers/instance.test.ts index 183b8ba38..63b5cbebb 100644 --- a/src/reducers/instance.test.ts +++ b/src/reducers/instance.test.ts @@ -1,5 +1,4 @@ import { ADMIN_CONFIG_UPDATE_REQUEST } from 'soapbox/actions/admin'; -import { rememberInstance } from 'soapbox/actions/instance'; import reducer from './instance'; @@ -31,100 +30,6 @@ describe('instance reducer', () => { expect(result).toMatchObject(expected); }); - describe('rememberInstance.fulfilled', () => { - it('normalizes Pleroma instance with Mastodon configuration format', async () => { - const payload = await import('soapbox/__fixtures__/pleroma-instance.json'); - - const action = { - type: rememberInstance.fulfilled.type, - payload, - }; - - const result = reducer(undefined, action); - - const expected = { - configuration: { - statuses: { - max_characters: 5000, - max_media_attachments: Infinity, - }, - polls: { - max_options: 20, - max_characters_per_option: 200, - min_expiration: 0, - max_expiration: 31536000, - }, - }, - }; - - expect(result).toMatchObject(expected); - }); - - it('normalizes Mastodon instance with retained configuration', async () => { - const payload = await import('soapbox/__fixtures__/mastodon-instance.json'); - - const action = { - type: rememberInstance.fulfilled.type, - payload, - }; - - const result = reducer(undefined, action); - - const expected = { - configuration: { - statuses: { - max_characters: 500, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, - media_attachments: { - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, - polls: { - max_options: 4, - max_characters_per_option: 50, - min_expiration: 300, - max_expiration: 2629746, - }, - }, - }; - - expect(result).toMatchObject(expected); - }); - - it('normalizes Mastodon 3.0.0 instance with default configuration', async () => { - const payload = await import('soapbox/__fixtures__/mastodon-3.0.0-instance.json'); - - const action = { - type: rememberInstance.fulfilled.type, - payload, - }; - - const result = reducer(undefined, action); - - const expected = { - configuration: { - statuses: { - max_characters: 500, - max_media_attachments: 4, - }, - polls: { - max_options: 4, - max_characters_per_option: 25, - min_expiration: 300, - max_expiration: 2629746, - }, - }, - }; - - expect(result).toMatchObject(expected); - }); - }); - describe('ADMIN_CONFIG_UPDATE_REQUEST', async () => { const { configs } = await import('soapbox/__fixtures__/pleroma-admin-config.json'); diff --git a/src/reducers/instance.ts b/src/reducers/instance.ts index a6524207e..c91bf6107 100644 --- a/src/reducers/instance.ts +++ b/src/reducers/instance.ts @@ -8,7 +8,6 @@ import KVStore from 'soapbox/storage/kv-store'; import { ConfigDB } from 'soapbox/utils/config-db'; import { - rememberInstance, fetchInstance, fetchInstanceV2, } from '../actions/instance'; @@ -18,11 +17,16 @@ import type { APIEntity } from 'soapbox/types/entities'; const initialState: Instance = instanceSchema.parse({}); -const importInstance = (_state: typeof initialState, instance: APIEntity) => { +const importInstance = (_state: Instance, instance: APIEntity): Instance => { return instanceSchema.parse(instance); }; -const preloadImport = (state: typeof initialState, action: Record, path: string) => { +const importInstanceV2 = (state: Instance, data: APIEntity): Instance => { + const instance = instanceSchema.parse(data); + return { ...instance, stats: state.stats }; +}; + +const preloadImport = (state: Instance, action: Record, path: string) => { const instance = action.data[path]; return instance ? importInstance(state, instance) : state; }; @@ -34,7 +38,7 @@ const getConfigValue = (instanceConfig: ImmutableMap, key: string) return v ? v.getIn(['tuple', 1]) : undefined; }; -const importConfigs = (state: typeof initialState, configs: ImmutableList) => { +const importConfigs = (state: Instance, configs: ImmutableList) => { // FIXME: This is pretty hacked together. Need to make a cleaner map. const config = ConfigDB.find(configs, ':pleroma', ':instance'); const simplePolicy = ConfigDB.toSimplePolicy(configs); @@ -59,7 +63,7 @@ const importConfigs = (state: typeof initialState, configs: ImmutableList) }); }; -const handleAuthFetch = (state: typeof initialState) => { +const handleAuthFetch = (state: Instance) => { // Authenticated fetch is enabled, so make the instance appear censored return { ...state, @@ -86,7 +90,13 @@ const persistInstance = (instance: { uri: string }, host: string | null = getHos } }; -const handleInstanceFetchFail = (state: typeof initialState, error: Record) => { +const persistInstanceV2 = (instance: { uri: string }, host: string | null = getHost(instance)) => { + if (host) { + KVStore.setItem(`instanceV2:${host}`, instance).catch(console.error); + } +}; + +const handleInstanceFetchFail = (state: Instance, error: Record) => { if (error.response?.status === 401) { return handleAuthFetch(state); } else { @@ -98,14 +108,13 @@ export default function instance(state = initialState, action: AnyAction) { switch (action.type) { case PLEROMA_PRELOAD_IMPORT: return preloadImport(state, action, '/api/v1/instance'); - case rememberInstance.fulfilled.type: - return importInstance(state, action.payload.instance); case fetchInstance.fulfilled.type: - case fetchInstanceV2.fulfilled.type: persistInstance(action.payload); return importInstance(state, action.payload.instance); + case fetchInstanceV2.fulfilled.type: + persistInstanceV2(action.payload); + return importInstanceV2(state, action.payload.instance); case fetchInstance.rejected.type: - case fetchInstanceV2.rejected.type: return handleInstanceFetchFail(state, action.error); case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_SUCCESS: