Merge remote-tracking branch 'origin/main' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
cdbfbf95ae
28 changed files with 510 additions and 473 deletions
|
@ -1,10 +1,11 @@
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
import { gte } from 'semver';
|
||||||
|
|
||||||
import KVStore from 'soapbox/storage/kv-store';
|
import KVStore from 'soapbox/storage/kv-store';
|
||||||
import { RootState } from 'soapbox/store';
|
import { RootState } from 'soapbox/store';
|
||||||
import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth';
|
import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth';
|
||||||
import { parseVersion } from 'soapbox/utils/features';
|
import { MASTODON, parseVersion, PLEROMA, REBASED } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
|
@ -22,25 +23,50 @@ export const getHost = (state: RootState) => {
|
||||||
export const rememberInstance = createAsyncThunk(
|
export const rememberInstance = createAsyncThunk(
|
||||||
'instance/remember',
|
'instance/remember',
|
||||||
async(host: string) => {
|
async(host: string) => {
|
||||||
return await KVStore.getItemOrError(`instance:${host}`);
|
const instance = await KVStore.getItemOrError(`instance:${host}`);
|
||||||
|
|
||||||
|
return { instance, host };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const supportsInstanceV2 = (instance: Record<string, any>): boolean => {
|
||||||
|
const v = parseVersion(get(instance, 'version'));
|
||||||
|
return (v.software === MASTODON && gte(v.compatVersion, '4.0.0')) ||
|
||||||
|
(v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.5.54'));
|
||||||
|
};
|
||||||
|
|
||||||
/** We may need to fetch nodeinfo on Pleroma < 2.1 */
|
/** We may need to fetch nodeinfo on Pleroma < 2.1 */
|
||||||
const needsNodeinfo = (instance: Record<string, any>): boolean => {
|
const needsNodeinfo = (instance: Record<string, any>): boolean => {
|
||||||
const v = parseVersion(get(instance, 'version'));
|
const v = parseVersion(get(instance, 'version'));
|
||||||
return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']);
|
return v.software === PLEROMA && !get(instance, ['pleroma', 'metadata']);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchInstance = createAsyncThunk<void, void, { state: RootState }>(
|
export const fetchInstance = createAsyncThunk<{ instance: Record<string, any>; host?: string | null }, string | null | undefined, { state: RootState }>(
|
||||||
'instance/fetch',
|
'instance/fetch',
|
||||||
async(_arg, { dispatch, getState, rejectWithValue }) => {
|
async(host, { dispatch, getState, rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const { data: instance } = await api(getState).get('/api/v1/instance');
|
const { data: instance } = await api(getState).get('/api/v1/instance');
|
||||||
|
|
||||||
|
if (supportsInstanceV2(instance)) {
|
||||||
|
return dispatch(fetchInstanceV2(host)) as any as { instance: Record<string, any>; host?: string | null };
|
||||||
|
}
|
||||||
|
|
||||||
if (needsNodeinfo(instance)) {
|
if (needsNodeinfo(instance)) {
|
||||||
dispatch(fetchNodeinfo());
|
dispatch(fetchNodeinfo());
|
||||||
}
|
}
|
||||||
return instance;
|
return { instance, host };
|
||||||
|
} catch (e) {
|
||||||
|
return rejectWithValue(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchInstanceV2 = createAsyncThunk<{ instance: Record<string, any>; host?: string | null }, string | null | undefined, { state: RootState }>(
|
||||||
|
'instance/fetch',
|
||||||
|
async(host, { getState, rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const { data: instance } = await api(getState).get('/api/v2/instance');
|
||||||
|
return { instance, host };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return rejectWithValue(e);
|
return rejectWithValue(e);
|
||||||
}
|
}
|
||||||
|
@ -52,10 +78,13 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
'instance/load',
|
'instance/load',
|
||||||
async(_arg, { dispatch, getState }) => {
|
async(_arg, { dispatch, getState }) => {
|
||||||
const host = getHost(getState());
|
const host = getHost(getState());
|
||||||
await Promise.all([
|
const rememberedInstance = await dispatch(rememberInstance(host || ''));
|
||||||
dispatch(rememberInstance(host || '')),
|
|
||||||
dispatch(fetchInstance()),
|
if (rememberedInstance.payload && supportsInstanceV2((rememberedInstance.payload as any).instance)) {
|
||||||
]);
|
await dispatch(fetchInstanceV2(host));
|
||||||
|
} else {
|
||||||
|
await dispatch(fetchInstance(host));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { buildGroup } from 'soapbox/jest/factory';
|
import { buildGroup } from 'soapbox/jest/factory';
|
||||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { useGroups } from './useGroups';
|
import { useGroups } from './useGroups';
|
||||||
|
|
||||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { __stub } from 'soapbox/api';
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { buildAccount, buildGroup } from 'soapbox/jest/factory';
|
import { buildAccount, buildGroup } from 'soapbox/jest/factory';
|
||||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { usePendingGroups } from './usePendingGroups';
|
import { usePendingGroups } from './usePendingGroups';
|
||||||
|
|
||||||
const id = '1';
|
const id = '1';
|
||||||
const group = buildGroup({ id, display_name: 'soapbox' });
|
const group = buildGroup({ id, display_name: 'soapbox' });
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||||
}),
|
}),
|
||||||
me: '1',
|
me: '1',
|
||||||
|
|
|
@ -14,7 +14,7 @@ function useTimelineStream(...args: Parameters<typeof connectTimelineStream>) {
|
||||||
const stream = useRef<(() => void) | null>(null);
|
const stream = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const accessToken = useAppSelector(getAccessToken);
|
const accessToken = useAppSelector(getAccessToken);
|
||||||
const streamingUrl = instance.urls?.streaming_api;
|
const streamingUrl = instance.configuration.urls.streaming;
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (enabled && streamingUrl && !stream.current) {
|
if (enabled && streamingUrl && !stream.current) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet, is } from 'immutable';
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
@ -7,8 +7,6 @@ import { fetchUsers } from 'soapbox/actions/admin';
|
||||||
import { Widget } from 'soapbox/components/ui';
|
import { Widget } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
import AccountContainer from 'soapbox/containers/account-container';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { selectAccount } from 'soapbox/selectors';
|
|
||||||
import { compareId } from 'soapbox/utils/comparators';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||||
|
@ -23,9 +21,7 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
||||||
const hasDates = useAppSelector((state) => accountIds.every(id => !!selectAccount(state, id)?.created_at));
|
|
||||||
|
|
||||||
const [total, setTotal] = useState(accountIds.size);
|
const [total, setTotal] = useState(accountIds.size);
|
||||||
|
|
||||||
|
@ -37,13 +33,6 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sortedIds = accountIds.sort(compareId).reverse();
|
|
||||||
const isSorted = hasDates && is(accountIds, sortedIds);
|
|
||||||
|
|
||||||
if (!isSorted || !accountIds || accountIds.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
history.push('/soapbox/admin/users');
|
history.push('/soapbox/admin/users');
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,9 +27,9 @@ const generateConfig = (mode: RegistrationMode) => {
|
||||||
}];
|
}];
|
||||||
};
|
};
|
||||||
|
|
||||||
const modeFromInstance = (instance: Instance): RegistrationMode => {
|
const modeFromInstance = ({ registrations }: Instance): RegistrationMode => {
|
||||||
if (instance.approval_required && instance.registrations) return 'approval';
|
if (registrations.approval_required && registrations.enabled) return 'approval';
|
||||||
return instance.registrations ? 'open' : 'closed';
|
return registrations.enabled ? 'open' : 'closed';
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Allows changing the registration mode of the instance, eg "open", "closed", "approval" */
|
/** Allows changing the registration mode of the instance, eg "open", "closed", "approval" */
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { fireEvent, render, screen } from 'soapbox/jest/test-helpers';
|
import { fireEvent, render, screen } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import LoginForm from './login-form';
|
import LoginForm from './login-form';
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ describe('<LoginForm />', () => {
|
||||||
it('renders for Pleroma', () => {
|
it('renders for Pleroma', () => {
|
||||||
const mockFn = vi.fn();
|
const mockFn = vi.fn();
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@ describe('<LoginForm />', () => {
|
||||||
it('renders for Mastodon', () => {
|
it('renders for Mastodon', () => {
|
||||||
const mockFn = vi.fn();
|
const mockFn = vi.fn();
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.0.0',
|
version: '3.0.0',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import LoginPage from './login-page';
|
import LoginPage from './login-page';
|
||||||
|
|
||||||
describe('<LoginPage />', () => {
|
describe('<LoginPage />', () => {
|
||||||
it('renders correctly on load', () => {
|
it('renders correctly on load', () => {
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
||||||
|
|
||||||
const locale = settings.get('locale');
|
const locale = settings.get('locale');
|
||||||
const needsConfirmation = instance.pleroma.metadata.account_activation_required;
|
const needsConfirmation = instance.pleroma.metadata.account_activation_required;
|
||||||
const needsApproval = instance.approval_required;
|
const needsApproval = instance.registrations.approval_required;
|
||||||
const supportsEmailList = features.emailList;
|
const supportsEmailList = features.emailList;
|
||||||
const supportsAccountLookup = features.accountLookup;
|
const supportsAccountLookup = features.accountLookup;
|
||||||
const birthdayRequired = instance.pleroma.metadata.birthday_required;
|
const birthdayRequired = instance.pleroma.metadata.birthday_required;
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface IUploadCompose {
|
||||||
|
|
||||||
const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit }) => {
|
const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { description_limit: descriptionLimit } = useInstance();
|
const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance();
|
||||||
|
|
||||||
const media = useCompose(composeId).media_attachments.find(item => item.id === id)!;
|
const media = useCompose(composeId).media_attachments.find(item => item.id === id)!;
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ import React from 'react';
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { buildGroup } from 'soapbox/jest/factory';
|
import { buildGroup } from 'soapbox/jest/factory';
|
||||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import Search from './search';
|
import Search from './search';
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { buildAccount } from 'soapbox/jest/factory';
|
import { buildAccount } from 'soapbox/jest/factory';
|
||||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import Discover from './discover';
|
import Discover from './discover';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ const store: any = {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||||
software: 'TRUTHSOCIAL',
|
software: 'TRUTHSOCIAL',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { LogoText } from './logo-text';
|
||||||
|
|
||||||
const SiteBanner: React.FC = () => {
|
const SiteBanner: React.FC = () => {
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
const description = instance.short_description || instance.description;
|
const description = instance.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={3}>
|
<Stack space={3}>
|
||||||
|
|
|
@ -43,6 +43,7 @@ const languages = {
|
||||||
is: 'íslenska',
|
is: 'íslenska',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
|
jv: 'ꦧꦱꦗꦮ',
|
||||||
ka: 'ქართული',
|
ka: 'ქართული',
|
||||||
kk: 'Қазақша',
|
kk: 'Қазақша',
|
||||||
ko: '한국어',
|
ko: '한국어',
|
||||||
|
|
|
@ -342,7 +342,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
|
|
||||||
<WrappedRoute path='/about/:slug?' page={DefaultPage} component={AboutPage} publicRoute exact />
|
<WrappedRoute path='/about/:slug?' page={DefaultPage} component={AboutPage} publicRoute exact />
|
||||||
|
|
||||||
{(features.accountCreation && instance.registrations) && (
|
{(features.accountCreation && instance.registrations.enabled) && (
|
||||||
<WrappedRoute path='/signup' page={EmptyPage} component={RegistrationPage} publicRoute exact />
|
<WrappedRoute path='/signup' page={EmptyPage} component={RegistrationPage} publicRoute exact />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { buildAccount, buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
import { buildAccount, buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
||||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { useGroupsPath } from './useGroupsPath';
|
import { useGroupsPath } from './useGroupsPath';
|
||||||
|
|
||||||
describe('useGroupsPath()', () => {
|
describe('useGroupsPath()', () => {
|
||||||
test('without the groupsDiscovery feature', () => {
|
test('without the groupsDiscovery feature', () => {
|
||||||
const store = {
|
const store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -24,7 +24,7 @@ describe('useGroupsPath()', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const userId = '1';
|
const userId = '1';
|
||||||
store = {
|
store = {
|
||||||
instance: normalizeInstance({
|
instance: instanceSchema.parse({
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||||
}),
|
}),
|
||||||
me: userId,
|
me: userId,
|
||||||
|
|
|
@ -7,6 +7,6 @@ export const useRegistrationStatus = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** Registrations are open. */
|
/** Registrations are open. */
|
||||||
isOpen: features.accountCreation && instance.registrations,
|
isOpen: features.accountCreation && instance.registrations.enabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -1,13 +1,13 @@
|
||||||
import alexJson from 'soapbox/__fixtures__/pleroma-account.json';
|
import alexJson from 'soapbox/__fixtures__/pleroma-account.json';
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
import { instanceSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { buildAccount } from './factory';
|
import { buildAccount } from './factory';
|
||||||
|
|
||||||
/** Store with registrations open. */
|
/** Store with registrations open. */
|
||||||
const storeOpen = { instance: normalizeInstance({ registrations: true }) };
|
const storeOpen = { instance: instanceSchema.parse({ registrations: true }) };
|
||||||
|
|
||||||
/** Store with registrations closed. */
|
/** Store with registrations closed. */
|
||||||
const storeClosed = { instance: normalizeInstance({ registrations: false }) };
|
const storeClosed = { instance: instanceSchema.parse({ registrations: false }) };
|
||||||
|
|
||||||
/** Store with a logged-in user. */
|
/** Store with a logged-in user. */
|
||||||
const storeLoggedIn = {
|
const storeLoggedIn = {
|
||||||
|
|
|
@ -43,5 +43,78 @@
|
||||||
"account.profile": "ꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
"account.profile": "ꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
"account.profile_external": "ꦥꦿꦶꦏ꧀ꦱꦤꦶꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀ꦲꦶꦁ {domain}",
|
"account.profile_external": "ꦥꦿꦶꦏ꧀ꦱꦤꦶꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀ꦲꦶꦁ {domain}",
|
||||||
"account.register": "ꦣꦥ꦳꧀ꦠꦂ",
|
"account.register": "ꦣꦥ꦳꧀ꦠꦂ",
|
||||||
"account.remote_follow": "ꦠꦸꦠ꧀ꦏꦺꦭꦸꦩꦤ꧀ꦠꦂꦫꦺꦩꦺꦴꦠ꧀"
|
"account.remote_follow": "ꦠꦸꦠ꧀ꦏꦺꦭꦸꦩꦤ꧀ꦠꦂꦫꦺꦩꦺꦴꦠ꧀",
|
||||||
|
"account.remove_from_followers": "ꦧꦸꦱꦼꦏ꧀ꦱ꧀ꦏꦶꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.report": "ꦭꦥꦺꦴꦂꦏꦺ @{name}",
|
||||||
|
"account.requested": "ꦔꦼꦤ꧀ꦠꦺꦤꦶꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀꧉ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦩ꧀ꦧꦠꦭ꧀ꦏꦺꦥꦚꦸꦮꦸꦤ꧀ꦔꦼꦠꦸꦠ꧀ꦏꦺ꧉",
|
||||||
|
"account.requested_small": "ꦔꦼꦤ꧀ꦠꦺꦤꦶꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀",
|
||||||
|
"account.search": "ꦒꦺꦴꦭꦺꦏꦶꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.search_self": "ꦒꦺꦴꦭꦺꦏꦶꦥꦺꦴꦱ꧀ꦠꦶꦁꦔꦤ꧀ꦩꦸ",
|
||||||
|
"account.share": "ꦥꦿꦺꦴꦥ꦳ꦶꦭꦺ @{name} ꦧꦒꦶꦏꦼꦤ",
|
||||||
|
"account.show_reblogs": "ꦠꦩ꧀ꦥꦶꦭ꧀ꦏꦺꦭꦥꦺꦴꦫꦤ꧀ꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.subscribe": "ꦭꦁꦒꦤꦤ꧀ꦥꦼꦥꦺꦭꦶꦁꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.subscribe.success": "ꦥꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦄꦏꦸꦤ꧀ꦥꦸꦤꦶꦏꦶ꧉",
|
||||||
|
"account.unblock": "ꦮꦸꦫꦸꦁꦏꦺꦩ꧀ꦧꦼꦤ꧀ꦢꦸꦁ @{name}",
|
||||||
|
"account.unblock_domain": "ꦮꦸꦫꦸꦁꦤ꧀ꦝꦼꦭꦶꦏꦺ {domain}",
|
||||||
|
"account.unendorse": "ꦲꦩ꧀ꦥꦸꦤ꧀ꦒꦤ꧀ꦝꦺꦁꦠꦼꦁꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.unendorse.success": "ꦥꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦧꦺꦴꦠꦼꦤ꧀ꦒꦤ꧀ꦝꦺꦁ",
|
||||||
|
"account.unfollow": "ꦮꦸꦫꦸꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.unmute": "ꦮꦸꦫꦸꦁꦩ꧀ꦧꦸꦁꦏꦼꦩ꧀ @{name}",
|
||||||
|
"account.unsubscribe": "ꦮꦸꦫꦸꦁꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦥꦼꦥꦺꦭꦶꦁꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.unsubscribe.success": "ꦥꦚꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦧꦺꦴꦠꦼꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦄꦏꦸꦤ꧀ꦥꦸꦤꦶꦏꦶ",
|
||||||
|
"account.verified": "ꦄꦏꦸꦤ꧀ꦢꦶꦥ꦳ꦺꦫꦶꦥ꦳ꦶꦏꦱꦶ",
|
||||||
|
"account_gallery.none": "ꦧꦺꦴꦠꦼꦤ꧀ꦮꦺꦴꦤ꧀ꦠꦼꦤ꧀ꦩꦺꦝꦶꦪꦏꦁꦢꦶꦠꦩ꧀ꦥꦶꦭ꧀ꦏꦺ.",
|
||||||
|
"account_moderation_modal.admin_fe": "ꦧꦸꦏꦲꦶꦁ AdminFE",
|
||||||
|
"account_moderation_modal.fields.account_role": "ꦠꦶꦁꦏꦠ꧀ꦱ꧀ꦠꦥ꦳꧀",
|
||||||
|
"account_moderation_modal.fields.deactivate": "ꦥꦠꦺꦤꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.fields.delete": "ꦧꦸꦱꦼꦏ꧀ꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.fields.verified": "ꦥꦠꦺꦤꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.roles.admin": "ꦄꦝ꧀ꦩꦶꦤ꧀",
|
||||||
|
"account_moderation_modal.roles.moderator": "ꦩꦺꦴꦝꦼꦫꦠꦺꦴꦂ",
|
||||||
|
"account_moderation_modal.roles.user": "ꦥꦔꦒꦼꦩ꧀",
|
||||||
|
"account_moderation_modal.title": "ꦩꦺꦴꦝꦺꦫꦠ꧀ @{acct}",
|
||||||
|
"account_note.header": "ꦕꦛꦼꦠꦤ꧀",
|
||||||
|
"account_note.placeholder": "ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦤꦩ꧀ꦧꦃꦕꦛꦼꦠꦤ꧀",
|
||||||
|
"account_search.placeholder": "ꦒꦺꦴꦭꦺꦏꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"actualStatus.edited": "ꦏꦎꦮꦲꦶ {date}",
|
||||||
|
"actualStatuses.quote_tombstone": "ꦏꦶꦫꦶꦩꦤ꧀ꦲꦺꦴꦫꦏꦱꦼꦝꦶꦪ.",
|
||||||
|
"admin.announcements.action": "ꦒꦮꦺꦮꦼꦮꦫ",
|
||||||
|
"admin.announcements.all_day": "ꦱꦧꦤ꧀ꦲꦫꦶ",
|
||||||
|
"admin.announcements.delete": "ꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.announcements.edit": "ꦎꦮꦲꦶ",
|
||||||
|
"admin.announcements.ends_at": "ꦩꦸꦁꦏꦱꦶꦲꦶꦁ:",
|
||||||
|
"admin.announcements.starts_at": "ꦢꦶꦮꦶꦮꦶꦠꦶꦲꦶꦁ:",
|
||||||
|
"admin.dashboard.registration_mode.approval_label": "ꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀ꦢꦶꦧꦸꦠꦸꦲꦏꦺ",
|
||||||
|
"admin.dashboard.registration_mode.closed_label": "ꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.dashboard.registration_mode.open_hint": "ꦱꦶꦤ꧀ꦠꦼꦤ꧀ꦏꦺꦩꦮꦺꦴꦤ꧀ꦱꦒꦼꦢ꧀ꦒꦧꦸꦁ.",
|
||||||
|
"admin.dashboard.registration_mode.open_label": "ꦧꦸꦏꦏ꧀",
|
||||||
|
"admin.dashboard.registration_mode_label": "ꦥꦤ꧀ꦝꦥ꦳꧀ꦠꦫꦤ꧀",
|
||||||
|
"admin.dashboard.settings_saved": "ꦥꦿꦤꦠꦤ꧀ꦏꦱꦶꦩ꧀ꦥꦼꦤ꧀!",
|
||||||
|
"admin.dashcounters.mau_label": "ꦥꦔꦒꦼꦩ꧀ꦄꦏ꧀ꦠꦶꦥ꦳꧀ꦮꦸꦭꦤꦤ꧀",
|
||||||
|
"admin.dashcounters.status_count_label": "ꦥꦺꦴꦱ꧀ꦠꦶꦤ꧀",
|
||||||
|
"admin.dashcounters.user_count_label": "ꦠꦺꦴꦠꦭ꧀ꦥꦔꦒꦼꦩ꧀",
|
||||||
|
"admin.dashwidgets.email_list_header": "ꦝꦥ꦳꧀ꦠꦂꦆꦩꦻꦭ꧀",
|
||||||
|
"admin.dashwidgets.software_header": "ꦥꦶꦫꦤ꧀ꦠꦶꦄꦭꦸꦱ꧀",
|
||||||
|
"admin.edit_announcement.created": "ꦮꦼꦮꦫꦢꦶꦥꦸꦤ꧀ꦢꦩꦼꦭ꧀",
|
||||||
|
"admin.edit_announcement.deleted": "ꦮꦫꦮꦫꦢꦶꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.edit_announcement.fields.all_day_label": "ꦄꦕꦫꦱꦧꦼꦤ꧀ꦲꦫꦶ",
|
||||||
|
"admin.edit_announcement.fields.content_label": "ꦏꦺꦴꦤ꧀ꦠꦺꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.content_placeholder": "ꦏꦺꦴꦤ꧀ꦠꦺꦤ꧀ꦮꦫꦮꦫ",
|
||||||
|
"admin.edit_announcement.fields.end_time_label": "ꦠꦁꦒꦭ꧀ꦥꦸꦁꦏꦱꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.end_time_placeholder": "ꦮꦫꦮꦫꦫꦩ꧀ꦥꦸꦁꦲꦶꦁ:",
|
||||||
|
"admin.edit_announcement.fields.start_time_label": "ꦠꦁꦒꦭ꧀ꦮꦶꦮꦶꦠꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.start_time_placeholder": "ꦮꦫꦮꦫꦢꦶꦮꦶꦮꦶꦠꦶꦲꦶꦁ:",
|
||||||
|
"admin.edit_announcement.save": "ꦱꦶꦩ꧀ꦥꦼꦤ꧀",
|
||||||
|
"admin.edit_announcement.updated": "ꦮꦫꦮꦫꦏꦈꦮꦲꦶ",
|
||||||
|
"admin.latest_accounts_panel.more": "ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦩꦶꦂꦱꦤꦶ {count, plural, one {# account} other {# accounts}}",
|
||||||
|
"admin.latest_accounts_panel.title": "ꦄꦏꦸꦤ꧀ꦥꦭꦶꦁꦄꦚꦂ",
|
||||||
|
"admin.reports.actions.close": "ꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.reports.actions.view_status": "ꦥꦶꦂꦱꦤꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀",
|
||||||
|
"admin.reports.report_closed_message": "ꦭꦥꦺꦴꦫꦤ꧀ꦧꦧꦒꦤ꧀ @{name} ꦢꦶꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.reports.report_title": "ꦭꦥꦺꦴꦫꦤꦲꦶꦁ {acct}",
|
||||||
|
"admin.statuses.actions.delete_status": "ꦧꦸꦱꦼꦏ꧀ꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀",
|
||||||
|
"admin.statuses.actions.mark_status_not_sensitive": "ꦠꦼꦔꦼꦫꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀ꦧꦺꦴꦠꦼꦤ꧀ꦒꦮꦠ꧀",
|
||||||
|
"admin.statuses.actions.mark_status_sensitive": "ꦠꦼꦔꦼꦫꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀ꦒꦮꦠ꧀",
|
||||||
|
"admin.statuses.status_deleted_message": "ꦏꦶꦫꦶꦩꦤ꧀ꦢꦤꦶꦁ @{acct} ꦱꦩ꧀ꦥꦸꦤ꧀ꦢꦶꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.statuses.status_marked_message_not_sensitive": "ꦏꦶꦫꦶꦩꦤ꧀ꦢꦺꦤꦶꦁ @{acct} ꦢꦶꦥꦸꦤ꧀ꦠꦺꦼꦔꦼꦫꦶꦧꦺꦴꦠꦼꦤ꧀ꦒꦮꦠ꧀"
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
||||||
export { normalizeGroup } from './group';
|
export { normalizeGroup } from './group';
|
||||||
export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship';
|
export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship';
|
||||||
export { HistoryRecord, normalizeHistory } from './history';
|
export { HistoryRecord, normalizeHistory } from './history';
|
||||||
export { InstanceRecord, normalizeInstance } from './instance';
|
|
||||||
export { ListRecord, normalizeList } from './list';
|
export { ListRecord, normalizeList } from './list';
|
||||||
export { LocationRecord, normalizeLocation } from './location';
|
export { LocationRecord, normalizeLocation } from './location';
|
||||||
export { MentionRecord, normalizeMention } from './mention';
|
export { MentionRecord, normalizeMention } from './mention';
|
||||||
|
|
|
@ -1,214 +0,0 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { normalizeInstance } from './instance';
|
|
||||||
|
|
||||||
describe('normalizeInstance()', () => {
|
|
||||||
it('normalizes an empty Map', () => {
|
|
||||||
const expected = {
|
|
||||||
approval_required: false,
|
|
||||||
contact_account: {},
|
|
||||||
configuration: {
|
|
||||||
media_attachments: {},
|
|
||||||
chats: {
|
|
||||||
max_characters: 5000,
|
|
||||||
max_media_attachments: 1,
|
|
||||||
},
|
|
||||||
polls: {
|
|
||||||
max_options: 4,
|
|
||||||
max_characters_per_option: 25,
|
|
||||||
min_expiration: 300,
|
|
||||||
max_expiration: 2629746,
|
|
||||||
},
|
|
||||||
statuses: {
|
|
||||||
max_characters: 500,
|
|
||||||
max_media_attachments: 4,
|
|
||||||
},
|
|
||||||
groups: {
|
|
||||||
max_characters_name: 50,
|
|
||||||
max_characters_description: 160,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: '',
|
|
||||||
description_limit: 1500,
|
|
||||||
email: '',
|
|
||||||
feature_quote: false,
|
|
||||||
fedibird_capabilities: [],
|
|
||||||
invites_enabled: false,
|
|
||||||
languages: [],
|
|
||||||
login_message: '',
|
|
||||||
pleroma: {
|
|
||||||
metadata: {
|
|
||||||
account_activation_required: false,
|
|
||||||
birthday_min_age: 0,
|
|
||||||
birthday_required: false,
|
|
||||||
features: [],
|
|
||||||
federation: {
|
|
||||||
enabled: true,
|
|
||||||
exclusions: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stats: {},
|
|
||||||
},
|
|
||||||
registrations: false,
|
|
||||||
rules: [],
|
|
||||||
short_description: '',
|
|
||||||
stats: {
|
|
||||||
domain_count: 0,
|
|
||||||
status_count: 0,
|
|
||||||
user_count: 0,
|
|
||||||
},
|
|
||||||
title: '',
|
|
||||||
thumbnail: '',
|
|
||||||
uri: '',
|
|
||||||
urls: {},
|
|
||||||
version: '0.0.0',
|
|
||||||
nostr: {
|
|
||||||
pubkey: undefined,
|
|
||||||
relay: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = normalizeInstance(ImmutableMap());
|
|
||||||
expect(result.toJS()).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Pleroma instance with Mastodon configuration format', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/pleroma-instance.json');
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
expect(result.toJS()).toMatchObject(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Mastodon instance with retained configuration', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/mastodon-instance.json');
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
expect(result.toJS()).toMatchObject(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Mastodon 3.0.0 instance with default configuration', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/mastodon-3.0.0-instance.json');
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
expect(result.toJS()).toMatchObject(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Fedibird instance', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/fedibird-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
// Sets description_limit
|
|
||||||
expect(result.description_limit).toEqual(1500);
|
|
||||||
|
|
||||||
// Preserves fedibird_capabilities
|
|
||||||
expect(result.fedibird_capabilities).toEqual(fromJS(instance.fedibird_capabilities));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Mitra instance', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/mitra-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
// Adds configuration and description_limit
|
|
||||||
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
|
|
||||||
expect(result.get('description_limit')).toBe(1500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes GoToSocial instance', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/gotosocial-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
// Normalizes max_toot_chars
|
|
||||||
expect(result.getIn(['configuration', 'statuses', 'max_characters'])).toEqual(5000);
|
|
||||||
expect(result.has('max_toot_chars')).toBe(false);
|
|
||||||
|
|
||||||
// Adds configuration and description_limit
|
|
||||||
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
|
|
||||||
expect(result.get('description_limit')).toBe(1500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Friendica instance', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/friendica-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
// Normalizes max_toot_chars
|
|
||||||
expect(result.getIn(['configuration', 'statuses', 'max_characters'])).toEqual(200000);
|
|
||||||
expect(result.has('max_toot_chars')).toBe(false);
|
|
||||||
|
|
||||||
// Adds configuration and description_limit
|
|
||||||
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
|
|
||||||
expect(result.get('description_limit')).toBe(1500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes a Mastodon RC version', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/mastodon-instance-rc.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
expect(result.version).toEqual('3.5.0-rc1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes Pixelfed instance', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/pixelfed-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
expect(result.title).toBe('pixelfed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renames Akkoma to Pleroma', async () => {
|
|
||||||
const instance = await import('soapbox/__fixtures__/akkoma-instance.json');
|
|
||||||
const result = normalizeInstance(instance);
|
|
||||||
|
|
||||||
expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.50+akkoma)');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,175 +0,0 @@
|
||||||
/**
|
|
||||||
* Instance normalizer:
|
|
||||||
* Converts API instances into our internal format.
|
|
||||||
* @see {@link https://docs.joinmastodon.org/entities/instance/}
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
List as ImmutableList,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
fromJS,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import { parseVersion, PLEROMA } from 'soapbox/utils/features';
|
|
||||||
import { mergeDefined } from 'soapbox/utils/normalizers';
|
|
||||||
import { isNumber } from 'soapbox/utils/numbers';
|
|
||||||
|
|
||||||
// Use Mastodon defaults
|
|
||||||
// https://docs.joinmastodon.org/entities/instance/
|
|
||||||
export const InstanceRecord = ImmutableRecord({
|
|
||||||
approval_required: false,
|
|
||||||
contact_account: ImmutableMap<string, any>(),
|
|
||||||
configuration: ImmutableMap<string, any>({
|
|
||||||
media_attachments: ImmutableMap<string, any>(),
|
|
||||||
chats: ImmutableMap<string, number>({
|
|
||||||
max_characters: 5000,
|
|
||||||
max_media_attachments: 1,
|
|
||||||
}),
|
|
||||||
polls: ImmutableMap<string, number>({
|
|
||||||
max_options: 4,
|
|
||||||
max_characters_per_option: 25,
|
|
||||||
min_expiration: 300,
|
|
||||||
max_expiration: 2629746,
|
|
||||||
}),
|
|
||||||
statuses: ImmutableMap<string, number>({
|
|
||||||
max_characters: 500,
|
|
||||||
max_media_attachments: 4,
|
|
||||||
}),
|
|
||||||
groups: ImmutableMap<string, number>({
|
|
||||||
max_characters_name: 50,
|
|
||||||
max_characters_description: 160,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
description: '',
|
|
||||||
description_limit: 1500,
|
|
||||||
email: '',
|
|
||||||
feature_quote: false,
|
|
||||||
fedibird_capabilities: ImmutableList(),
|
|
||||||
invites_enabled: false,
|
|
||||||
languages: ImmutableList(),
|
|
||||||
login_message: '',
|
|
||||||
pleroma: ImmutableMap<string, any>({
|
|
||||||
metadata: ImmutableMap<string, any>({
|
|
||||||
account_activation_required: false,
|
|
||||||
birthday_min_age: 0,
|
|
||||||
birthday_required: false,
|
|
||||||
features: ImmutableList(),
|
|
||||||
federation: ImmutableMap<string, any>({
|
|
||||||
enabled: true,
|
|
||||||
exclusions: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
stats: ImmutableMap(),
|
|
||||||
}),
|
|
||||||
registrations: false,
|
|
||||||
rules: ImmutableList(),
|
|
||||||
short_description: '',
|
|
||||||
stats: ImmutableMap<string, number>({
|
|
||||||
domain_count: 0,
|
|
||||||
status_count: 0,
|
|
||||||
user_count: 0,
|
|
||||||
}),
|
|
||||||
nostr: ImmutableMap<string, any>({
|
|
||||||
relay: undefined as string | undefined,
|
|
||||||
pubkey: undefined as string | undefined,
|
|
||||||
}),
|
|
||||||
title: '',
|
|
||||||
thumbnail: '',
|
|
||||||
uri: '',
|
|
||||||
urls: ImmutableMap<string, string>(),
|
|
||||||
version: '0.0.0',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build Mastodon configuration from Pleroma instance
|
|
||||||
const pleromaToMastodonConfig = (instance: ImmutableMap<string, any>) => {
|
|
||||||
return ImmutableMap({
|
|
||||||
statuses: ImmutableMap({
|
|
||||||
max_characters: instance.get('max_toot_chars'),
|
|
||||||
}),
|
|
||||||
polls: ImmutableMap({
|
|
||||||
max_options: instance.getIn(['poll_limits', 'max_options']),
|
|
||||||
max_characters_per_option: instance.getIn(['poll_limits', 'max_option_chars']),
|
|
||||||
min_expiration: instance.getIn(['poll_limits', 'min_expiration']),
|
|
||||||
max_expiration: instance.getIn(['poll_limits', 'max_expiration']),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the software's default attachment limit
|
|
||||||
const getAttachmentLimit = (software: string | null) => software === PLEROMA ? Infinity : 4;
|
|
||||||
|
|
||||||
// Normalize version
|
|
||||||
const normalizeVersion = (instance: ImmutableMap<string, any>) => {
|
|
||||||
return instance.update('version', '0.0.0', version => {
|
|
||||||
// Handle Mastodon release candidates
|
|
||||||
if (new RegExp(/[0-9.]+rc[0-9]+/g).test(version)) {
|
|
||||||
return version.split('rc').join('-rc');
|
|
||||||
} else {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Rename Akkoma to Pleroma+akkoma */
|
|
||||||
const fixAkkoma = (instance: ImmutableMap<string, any>) => {
|
|
||||||
const version: string = instance.get('version', '');
|
|
||||||
|
|
||||||
if (version.includes('Akkoma')) {
|
|
||||||
return instance.set('version', '2.7.2 (compatible; Pleroma 2.4.50+akkoma)');
|
|
||||||
} else {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Set Takahē version to a Pleroma-like string */
|
|
||||||
const fixTakahe = (instance: ImmutableMap<string, any>) => {
|
|
||||||
const version: string = instance.get('version', '');
|
|
||||||
|
|
||||||
if (version.startsWith('takahe/')) {
|
|
||||||
return instance.set('version', `0.0.0 (compatible; Takahe ${version.slice(7)})`);
|
|
||||||
} else {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fixBigBuffet = (instance: ImmutableMap<string, any>) => {
|
|
||||||
const version: string = instance.get('version', '');
|
|
||||||
|
|
||||||
if (version.includes('+bigbuffet')) {
|
|
||||||
return instance.set('version', version.replace('+bigbuffet', '+soapbox'));
|
|
||||||
} else {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
|
|
||||||
export const normalizeInstance = (instance: Record<string, any>) => {
|
|
||||||
return InstanceRecord(
|
|
||||||
ImmutableMap(fromJS(instance)).withMutations((instance: ImmutableMap<string, any>) => {
|
|
||||||
const { software } = parseVersion(instance.get('version'));
|
|
||||||
const mastodonConfig = pleromaToMastodonConfig(instance);
|
|
||||||
|
|
||||||
// Merge configuration
|
|
||||||
instance.update('configuration', ImmutableMap(), configuration => (
|
|
||||||
configuration.mergeDeepWith(mergeDefined, mastodonConfig)
|
|
||||||
));
|
|
||||||
|
|
||||||
// If max attachments isn't set, check the backend software
|
|
||||||
instance.updateIn(['configuration', 'statuses', 'max_media_attachments'], value => {
|
|
||||||
return isNumber(value) ? value : getAttachmentLimit(software);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Urls can't be null, fix for Friendica
|
|
||||||
if (instance.get('urls') === null) instance.delete('urls');
|
|
||||||
|
|
||||||
// Normalize version
|
|
||||||
normalizeVersion(instance);
|
|
||||||
fixTakahe(instance);
|
|
||||||
fixAkkoma(instance);
|
|
||||||
fixBigBuffet(instance);
|
|
||||||
|
|
||||||
// Merge defaults
|
|
||||||
instance.mergeDeepWith(mergeDefined, InstanceRecord());
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -10,13 +10,15 @@ import { ConfigDB } from 'soapbox/utils/config-db';
|
||||||
import {
|
import {
|
||||||
rememberInstance,
|
rememberInstance,
|
||||||
fetchInstance,
|
fetchInstance,
|
||||||
|
fetchInstanceV2,
|
||||||
} from '../actions/instance';
|
} from '../actions/instance';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const initialState: Instance = instanceSchema.parse({});
|
const initialState: Instance = instanceSchema.parse({});
|
||||||
|
|
||||||
const importInstance = (_state: typeof initialState, instance: unknown) => {
|
const importInstance = (_state: typeof initialState, instance: APIEntity) => {
|
||||||
return instanceSchema.parse(instance);
|
return instanceSchema.parse(instance);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,8 +47,10 @@ const importConfigs = (state: typeof initialState, configs: ImmutableList<any>)
|
||||||
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
|
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
|
||||||
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
|
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
|
||||||
|
|
||||||
draft.registrations = registrationsOpen ?? draft.registrations;
|
draft.registrations = {
|
||||||
draft.approval_required = approvalRequired ?? draft.approval_required;
|
enabled: registrationsOpen ?? draft.registrations.enabled,
|
||||||
|
approval_required: approvalRequired ?? draft.registrations.approval_required,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simplePolicy) {
|
if (simplePolicy) {
|
||||||
|
@ -76,9 +80,7 @@ const getHost = (instance: { uri: string }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistInstance = (instance: { uri: string }) => {
|
const persistInstance = (instance: { uri: string }, host: string | null = getHost(instance)) => {
|
||||||
const host = getHost(instance);
|
|
||||||
|
|
||||||
if (host) {
|
if (host) {
|
||||||
KVStore.setItem(`instance:${host}`, instance).catch(console.error);
|
KVStore.setItem(`instance:${host}`, instance).catch(console.error);
|
||||||
}
|
}
|
||||||
|
@ -97,11 +99,13 @@ export default function instance(state = initialState, action: AnyAction) {
|
||||||
case PLEROMA_PRELOAD_IMPORT:
|
case PLEROMA_PRELOAD_IMPORT:
|
||||||
return preloadImport(state, action, '/api/v1/instance');
|
return preloadImport(state, action, '/api/v1/instance');
|
||||||
case rememberInstance.fulfilled.type:
|
case rememberInstance.fulfilled.type:
|
||||||
return importInstance(state, action.payload);
|
return importInstance(state, action.payload.instance);
|
||||||
case fetchInstance.fulfilled.type:
|
case fetchInstance.fulfilled.type:
|
||||||
|
case fetchInstanceV2.fulfilled.type:
|
||||||
persistInstance(action.payload);
|
persistInstance(action.payload);
|
||||||
return importInstance(state, action.payload);
|
return importInstance(state, action.payload.instance);
|
||||||
case fetchInstance.rejected.type:
|
case fetchInstance.rejected.type:
|
||||||
|
case fetchInstanceV2.rejected.type:
|
||||||
return handleInstanceFetchFail(state, action.error);
|
return handleInstanceFetchFail(state, action.error);
|
||||||
case ADMIN_CONFIG_UPDATE_REQUEST:
|
case ADMIN_CONFIG_UPDATE_REQUEST:
|
||||||
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
||||||
|
|
214
src/schemas/instance.test.ts
Normal file
214
src/schemas/instance.test.ts
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
import { instanceSchema } from './instance';
|
||||||
|
|
||||||
|
describe('instanceSchema.parse()', () => {
|
||||||
|
it('normalizes an empty Map', () => {
|
||||||
|
const expected = {
|
||||||
|
configuration: {
|
||||||
|
media_attachments: {},
|
||||||
|
chats: {
|
||||||
|
max_characters: 5000,
|
||||||
|
max_media_attachments: 1,
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
max_characters_name: 50,
|
||||||
|
max_characters_description: 160,
|
||||||
|
},
|
||||||
|
polls: {
|
||||||
|
max_options: 4,
|
||||||
|
max_characters_per_option: 25,
|
||||||
|
min_expiration: 300,
|
||||||
|
max_expiration: 2629746,
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
max_characters: 500,
|
||||||
|
max_media_attachments: 4,
|
||||||
|
},
|
||||||
|
translation: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
urls: {},
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
domain: '',
|
||||||
|
feature_quote: false,
|
||||||
|
fedibird_capabilities: [],
|
||||||
|
languages: [],
|
||||||
|
pleroma: {
|
||||||
|
metadata: {
|
||||||
|
account_activation_required: false,
|
||||||
|
birthday_min_age: 0,
|
||||||
|
birthday_required: false,
|
||||||
|
description_limit: 1500,
|
||||||
|
features: [],
|
||||||
|
federation: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stats: {},
|
||||||
|
},
|
||||||
|
registrations: {
|
||||||
|
approval_required: false,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
stats: {},
|
||||||
|
title: '',
|
||||||
|
thumbnail: {
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
usage: {
|
||||||
|
users: {
|
||||||
|
active_month: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
version: '0.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instanceSchema.parse({});
|
||||||
|
expect(result).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Pleroma instance with Mastodon configuration format', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/pleroma-instance.json');
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
expect(result).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Mastodon instance with retained configuration', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/mastodon-instance.json');
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
expect(result).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Mastodon 3.0.0 instance with default configuration', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/mastodon-3.0.0-instance.json');
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
expect(result).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Fedibird instance', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/fedibird-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
// Sets description_limit
|
||||||
|
expect(result.pleroma.metadata.description_limit).toEqual(1500);
|
||||||
|
|
||||||
|
// Preserves fedibird_capabilities
|
||||||
|
expect(result.fedibird_capabilities).toEqual(instance.fedibird_capabilities);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Mitra instance', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/mitra-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
// Adds configuration and description_limit
|
||||||
|
expect(result.configuration).toBeTruthy();
|
||||||
|
expect(result.pleroma.metadata.description_limit).toBe(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes GoToSocial instance', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/gotosocial-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
// Normalizes max_toot_chars
|
||||||
|
expect(result.configuration.statuses.max_characters).toEqual(5000);
|
||||||
|
expect('max_toot_chars' in result).toBe(false);
|
||||||
|
|
||||||
|
// Adds configuration and description_limit
|
||||||
|
expect(result.configuration).toBeTruthy();
|
||||||
|
expect(result.pleroma.metadata.description_limit).toBe(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Friendica instance', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/friendica-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
// Normalizes max_toot_chars
|
||||||
|
expect(result.configuration.statuses.max_characters).toEqual(200000);
|
||||||
|
expect('max_toot_chars' in result).toBe(false);
|
||||||
|
|
||||||
|
// Adds configuration and description_limit
|
||||||
|
expect(result.configuration).toBeTruthy();
|
||||||
|
expect(result.pleroma.metadata.description_limit).toBe(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes a Mastodon RC version', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/mastodon-instance-rc.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
expect(result.version).toEqual('3.5.0-rc1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Pixelfed instance', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/pixelfed-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
expect(result.title).toBe('pixelfed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renames Akkoma to Pleroma', () => {
|
||||||
|
const instance = require('soapbox/__fixtures__/akkoma-instance.json');
|
||||||
|
const result = instanceSchema.parse(instance);
|
||||||
|
|
||||||
|
expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.50+akkoma)');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,11 +1,15 @@
|
||||||
/* eslint sort-keys: "error" */
|
/* eslint sort-keys: "error" */
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { PLEROMA, parseVersion } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import { accountSchema } from './account';
|
import { accountSchema } from './account';
|
||||||
import { mrfSimpleSchema } from './pleroma';
|
import { mrfSimpleSchema } from './pleroma';
|
||||||
import { ruleSchema } from './rule';
|
import { ruleSchema } from './rule';
|
||||||
import { coerceObject, filteredArray, mimeSchema } from './utils';
|
import { coerceObject, filteredArray, mimeSchema } from './utils';
|
||||||
|
|
||||||
|
const getAttachmentLimit = (software: string | null) => software === PLEROMA ? Infinity : 4;
|
||||||
|
|
||||||
const fixVersion = (version: string) => {
|
const fixVersion = (version: string) => {
|
||||||
// Handle Mastodon release candidates
|
// Handle Mastodon release candidates
|
||||||
if (new RegExp(/[0-9.]+rc[0-9]+/g).test(version)) {
|
if (new RegExp(/[0-9.]+rc[0-9]+/g).test(version)) {
|
||||||
|
@ -22,6 +26,10 @@ const fixVersion = (version: string) => {
|
||||||
version = `0.0.0 (compatible; Takahe ${version.slice(7)})`;
|
version = `0.0.0 (compatible; Takahe ${version.slice(7)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version.includes('+bigbuffet')) {
|
||||||
|
version = version.replace('+bigbuffet', '+soapbox');
|
||||||
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -53,9 +61,22 @@ const configurationSchema = coerceObject({
|
||||||
max_reactions: z.number().catch(0),
|
max_reactions: z.number().catch(0),
|
||||||
}),
|
}),
|
||||||
statuses: coerceObject({
|
statuses: coerceObject({
|
||||||
|
characters_reserved_per_url: z.number().optional().catch(undefined),
|
||||||
max_characters: z.number().optional().catch(undefined),
|
max_characters: z.number().optional().catch(undefined),
|
||||||
max_media_attachments: z.number().optional().catch(undefined),
|
max_media_attachments: z.number().optional().catch(undefined),
|
||||||
|
|
||||||
}),
|
}),
|
||||||
|
translation: coerceObject({
|
||||||
|
enabled: z.boolean().catch(false),
|
||||||
|
}),
|
||||||
|
urls: coerceObject({
|
||||||
|
streaming: z.string().url().optional().catch(undefined),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactSchema = coerceObject({
|
||||||
|
contact_account: accountSchema.optional().catch(undefined),
|
||||||
|
email: z.string().email().catch(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const nostrSchema = coerceObject({
|
const nostrSchema = coerceObject({
|
||||||
|
@ -68,6 +89,7 @@ const pleromaSchema = coerceObject({
|
||||||
account_activation_required: z.boolean().catch(false),
|
account_activation_required: z.boolean().catch(false),
|
||||||
birthday_min_age: z.number().catch(0),
|
birthday_min_age: z.number().catch(0),
|
||||||
birthday_required: z.boolean().catch(false),
|
birthday_required: z.boolean().catch(false),
|
||||||
|
description_limit: z.number().catch(1500),
|
||||||
features: z.string().array().catch([]),
|
features: z.string().array().catch([]),
|
||||||
federation: coerceObject({
|
federation: coerceObject({
|
||||||
enabled: z.boolean().catch(true), // Assume true unless explicitly false
|
enabled: z.boolean().catch(true), // Assume true unless explicitly false
|
||||||
|
@ -131,14 +153,20 @@ const pleromaPollLimitsSchema = coerceObject({
|
||||||
min_expiration: z.number().optional().catch(undefined),
|
min_expiration: z.number().optional().catch(undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
const statsSchema = coerceObject({
|
const registrations = coerceObject({
|
||||||
domain_count: z.number().catch(0),
|
approval_required: z.boolean().catch(false),
|
||||||
status_count: z.number().catch(0),
|
enabled: z.boolean().catch(false),
|
||||||
user_count: z.number().catch(0),
|
message: z.string().optional().catch(undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlsSchema = coerceObject({
|
const statsSchema = coerceObject({
|
||||||
streaming_api: z.string().url().optional().catch(undefined),
|
domain_count: z.number().optional().catch(undefined),
|
||||||
|
status_count: z.number().optional().catch(undefined),
|
||||||
|
user_count: z.number().optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const thumbnailSchema = coerceObject({
|
||||||
|
url: z.string().catch(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const usageSchema = coerceObject({
|
const usageSchema = coerceObject({
|
||||||
|
@ -147,7 +175,7 @@ const usageSchema = coerceObject({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const instanceSchema = coerceObject({
|
const instanceV1Schema = coerceObject({
|
||||||
approval_required: z.boolean().catch(false),
|
approval_required: z.boolean().catch(false),
|
||||||
configuration: configurationSchema,
|
configuration: configurationSchema,
|
||||||
contact_account: accountSchema.optional().catch(undefined),
|
contact_account: accountSchema.optional().catch(undefined),
|
||||||
|
@ -168,26 +196,106 @@ const instanceSchema = coerceObject({
|
||||||
stats: statsSchema,
|
stats: statsSchema,
|
||||||
thumbnail: z.string().catch(''),
|
thumbnail: z.string().catch(''),
|
||||||
title: z.string().catch(''),
|
title: z.string().catch(''),
|
||||||
urls: urlsSchema,
|
urls: coerceObject({
|
||||||
|
streaming_api: z.string().url().optional().catch(undefined),
|
||||||
|
}),
|
||||||
usage: usageSchema,
|
usage: usageSchema,
|
||||||
version: z.string().catch(''),
|
version: z.string().catch('0.0.0'),
|
||||||
}).transform(({ max_media_attachments, max_toot_chars, poll_limits, ...instance }) => {
|
});
|
||||||
const { configuration } = instance;
|
|
||||||
|
|
||||||
const version = fixVersion(instance.version);
|
const instanceSchema = z.preprocess((data: any) => {
|
||||||
|
if (data.domain) return data;
|
||||||
|
|
||||||
const polls = {
|
const {
|
||||||
|
approval_required,
|
||||||
|
configuration,
|
||||||
|
contact_account,
|
||||||
|
description,
|
||||||
|
description_limit,
|
||||||
|
email,
|
||||||
|
max_media_attachments,
|
||||||
|
max_toot_chars,
|
||||||
|
poll_limits,
|
||||||
|
pleroma,
|
||||||
|
registrations,
|
||||||
|
short_description,
|
||||||
|
thumbnail,
|
||||||
|
urls,
|
||||||
|
...instance
|
||||||
|
} = instanceV1Schema.parse(data);
|
||||||
|
|
||||||
|
const { software } = parseVersion(instance.version);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...instance,
|
||||||
|
configuration: {
|
||||||
|
...configuration,
|
||||||
|
polls: {
|
||||||
...configuration.polls,
|
...configuration.polls,
|
||||||
max_characters_per_option: configuration.polls.max_characters_per_option ?? poll_limits.max_option_chars ?? 25,
|
max_characters_per_option: configuration.polls.max_characters_per_option ?? poll_limits.max_option_chars ?? 25,
|
||||||
max_expiration: configuration.polls.max_expiration ?? poll_limits.max_expiration ?? 2629746,
|
max_expiration: configuration.polls.max_expiration ?? poll_limits.max_expiration ?? 2629746,
|
||||||
max_options: configuration.polls.max_options ?? poll_limits.max_options ?? 4,
|
max_options: configuration.polls.max_options ?? poll_limits.max_options ?? 4,
|
||||||
min_expiration: configuration.polls.min_expiration ?? poll_limits.min_expiration ?? 300,
|
min_expiration: configuration.polls.min_expiration ?? poll_limits.min_expiration ?? 300,
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
...configuration.statuses,
|
||||||
|
max_characters: configuration.statuses.max_characters ?? max_toot_chars ?? 500,
|
||||||
|
max_media_attachments: configuration.statuses.max_media_attachments ?? max_media_attachments ?? getAttachmentLimit(software),
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
streaming: urls.streaming_api,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
account: contact_account,
|
||||||
|
email: email,
|
||||||
|
},
|
||||||
|
description: short_description || description,
|
||||||
|
pleroma: {
|
||||||
|
...pleroma,
|
||||||
|
metadata: {
|
||||||
|
...pleroma.metadata,
|
||||||
|
description_limit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrations: {
|
||||||
|
approval_required: approval_required,
|
||||||
|
enabled: registrations,
|
||||||
|
},
|
||||||
|
thumbnail: { url: thumbnail },
|
||||||
|
};
|
||||||
|
}, coerceObject({
|
||||||
|
configuration: configurationSchema,
|
||||||
|
contact: contactSchema,
|
||||||
|
description: z.string().catch(''),
|
||||||
|
domain: z.string().catch(''),
|
||||||
|
feature_quote: z.boolean().catch(false),
|
||||||
|
fedibird_capabilities: z.array(z.string()).catch([]),
|
||||||
|
languages: z.string().array().catch([]),
|
||||||
|
nostr: nostrSchema.optional().catch(undefined),
|
||||||
|
pleroma: pleromaSchema,
|
||||||
|
registrations: registrations,
|
||||||
|
rules: filteredArray(ruleSchema),
|
||||||
|
stats: statsSchema,
|
||||||
|
thumbnail: thumbnailSchema,
|
||||||
|
title: z.string().catch(''),
|
||||||
|
usage: usageSchema,
|
||||||
|
version: z.string().catch('0.0.0'),
|
||||||
|
}).transform(({ configuration, ...instance }) => {
|
||||||
|
const version = fixVersion(instance.version);
|
||||||
|
|
||||||
|
const polls = {
|
||||||
|
...configuration.polls,
|
||||||
|
max_characters_per_option: configuration.polls.max_characters_per_option ?? 25,
|
||||||
|
max_expiration: configuration.polls.max_expiration ?? 2629746,
|
||||||
|
max_options: configuration.polls.max_options ?? 4,
|
||||||
|
min_expiration: configuration.polls.min_expiration ?? 300,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statuses = {
|
const statuses = {
|
||||||
...configuration.statuses,
|
...configuration.statuses,
|
||||||
max_characters: configuration.statuses.max_characters ?? max_toot_chars ?? 500,
|
max_characters: configuration.statuses.max_characters ?? 500,
|
||||||
max_media_attachments: configuration.statuses.max_media_attachments ?? max_media_attachments ?? 4,
|
max_media_attachments: configuration.statuses.max_media_attachments ?? 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -199,7 +307,7 @@ const instanceSchema = coerceObject({
|
||||||
},
|
},
|
||||||
version,
|
version,
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
|
|
||||||
type Instance = z.infer<typeof instanceSchema>;
|
type Instance = z.infer<typeof instanceSchema>;
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function connectStream(
|
||||||
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
|
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
|
||||||
) {
|
) {
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const streamingAPIBaseURL = getState().instance.urls.streaming_api;
|
const streamingAPIBaseURL = getState().instance.configuration.urls.streaming;
|
||||||
const accessToken = getAccessToken(getState());
|
const accessToken = getAccessToken(getState());
|
||||||
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
FilterKeywordRecord,
|
FilterKeywordRecord,
|
||||||
FilterStatusRecord,
|
FilterStatusRecord,
|
||||||
HistoryRecord,
|
HistoryRecord,
|
||||||
InstanceRecord,
|
|
||||||
ListRecord,
|
ListRecord,
|
||||||
LocationRecord,
|
LocationRecord,
|
||||||
MentionRecord,
|
MentionRecord,
|
||||||
|
@ -41,7 +40,6 @@ type Filter = ReturnType<typeof FilterRecord>;
|
||||||
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
||||||
type FilterStatus = ReturnType<typeof FilterStatusRecord>;
|
type FilterStatus = ReturnType<typeof FilterStatusRecord>;
|
||||||
type History = ReturnType<typeof HistoryRecord>;
|
type History = ReturnType<typeof HistoryRecord>;
|
||||||
type Instance = ReturnType<typeof InstanceRecord>;
|
|
||||||
type List = ReturnType<typeof ListRecord>;
|
type List = ReturnType<typeof ListRecord>;
|
||||||
type Location = ReturnType<typeof LocationRecord>;
|
type Location = ReturnType<typeof LocationRecord>;
|
||||||
type Mention = ReturnType<typeof MentionRecord>;
|
type Mention = ReturnType<typeof MentionRecord>;
|
||||||
|
@ -77,7 +75,6 @@ export {
|
||||||
FilterKeyword,
|
FilterKeyword,
|
||||||
FilterStatus,
|
FilterStatus,
|
||||||
History,
|
History,
|
||||||
Instance,
|
|
||||||
List,
|
List,
|
||||||
Location,
|
Location,
|
||||||
Mention,
|
Mention,
|
||||||
|
|
|
@ -244,6 +244,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
v.software === PIXELFED,
|
v.software === PIXELFED,
|
||||||
v.software === TAKAHE && gte(v.version, '0.9.0'),
|
v.software === TAKAHE && gte(v.version, '0.9.0'),
|
||||||
|
v.software === DITTO,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -649,6 +650,16 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
importData: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
importData: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mastodon server information API v2.
|
||||||
|
* @see GET /api/v2/instance
|
||||||
|
* @see {@link https://docs.joinmastodon.org/methods/instance/#v2}
|
||||||
|
*/
|
||||||
|
instanceV2: any([
|
||||||
|
v.software === MASTODON && gte(v.compatVersion, '4.0.0'),
|
||||||
|
v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.5.54'),
|
||||||
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can translate multiple statuses in a single request.
|
* Can translate multiple statuses in a single request.
|
||||||
* @see POST /api/v1/pleroma/statuses/translate
|
* @see POST /api/v1/pleroma/statuses/translate
|
||||||
|
@ -686,6 +697,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see POST /api/v1/admin/accounts/:account_id/approve
|
* @see POST /api/v1/admin/accounts/:account_id/approve
|
||||||
*/
|
*/
|
||||||
mastodonAdmin: any([
|
mastodonAdmin: any([
|
||||||
|
v.software === DITTO,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.9.1'),
|
v.software === MASTODON && gte(v.compatVersion, '2.9.1'),
|
||||||
v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.4.50'),
|
||||||
]),
|
]),
|
||||||
|
@ -968,7 +980,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* Can translate statuses.
|
* Can translate statuses.
|
||||||
* @see POST /api/v1/statuses/:id/translate
|
* @see POST /api/v1/statuses/:id/translate
|
||||||
*/
|
*/
|
||||||
translations: features.includes('translation'),
|
translations: features.includes('translation') || instance.configuration.translation.enabled,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trending statuses.
|
* Trending statuses.
|
||||||
|
@ -1038,7 +1050,7 @@ export const parseVersion = (version: string): Backend => {
|
||||||
build: semver.build[0],
|
build: semver.build[0],
|
||||||
compatVersion: compat.version,
|
compatVersion: compat.version,
|
||||||
software: match[2] || MASTODON,
|
software: match[2] || MASTODON,
|
||||||
version: semver.version,
|
version: semver.version.split('-')[0],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// If we can't parse the version, this is a new and exotic backend.
|
// If we can't parse the version, this is a new and exotic backend.
|
||||||
|
|
Loading…
Reference in a new issue