diff --git a/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json b/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json new file mode 100644 index 000000000..f1d0a5e6d --- /dev/null +++ b/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json @@ -0,0 +1,43 @@ +{ + "uri": "animalliberation.social", + "title": "Animal Liberation Network", + "short_description": "", + "description": "Animal Liberation Network is a community for animal activists on the Fediverse. You can connect with other activists through the local timeline, as well as spread your activism to the outside world with the federated timeline.", + "email": "alex@alexgleason.me", + "version": "3.0.0", + "urls": { + "streaming_api": "wss://animalliberation.social" + }, + "stats": { + "user_count": 662, + "status_count": 2904, + "domain_count": 4003 + }, + "thumbnail": "https://animalliberation.social/packs/media/images/preview-9a17d32fc48369e8ccd910a75260e67d.jpg", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "contact_account": { + "id": "1", + "username": "alex", + "acct": "alex", + "display_name": "Alex Gleason", + "locked": false, + "bot": false, + "created_at": "2016-11-30T22:19:42.956Z", + "note": "
Animal liberation free software Communist
", + "url": "https://animalliberation.social/@alex", + "avatar": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg", + "avatar_static": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg", + "header": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg", + "header_static": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg", + "followers_count": 236, + "following_count": 83, + "statuses_count": 357, + "last_status_at": "2021-02-20T19:28:24.353Z", + "emojis": [], + "fields": [] + } +} diff --git a/app/soapbox/__fixtures__/mastodon-instance.json b/app/soapbox/__fixtures__/mastodon-instance.json new file mode 100644 index 000000000..3c8a2f9d3 --- /dev/null +++ b/app/soapbox/__fixtures__/mastodon-instance.json @@ -0,0 +1,128 @@ +{ + "uri": "mastodon.social", + "title": "Mastodon", + "short_description": "Server run by the main developers of the projectFounder, CEO and lead developer @Mastodon, Germany.
", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 98343, + "following_count": 271, + "statuses_count": 71288, + "last_status_at": "2022-01-31", + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "https://www.patreon.com/mastodon", + "verified_at": null + }, + { + "name": "Homepage", + "value": "https://zeonfederated.com", + "verified_at": "2019-07-15T18:29:57.191+00:00" + } + ] + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "6", + "text": "No spam, advertising or bot accounts" + } + ] +} diff --git a/app/soapbox/__fixtures__/pleroma-instance.json b/app/soapbox/__fixtures__/pleroma-instance.json new file mode 100644 index 000000000..b91376302 --- /dev/null +++ b/app/soapbox/__fixtures__/pleroma-instance.json @@ -0,0 +1,131 @@ +{ + "approval_required": true, + "avatar_upload_limit": 2000000, + "background_image": "https://gleasonator.com/images/city.jpg", + "background_upload_limit": 4000000, + "banner_upload_limit": 4000000, + "description": "Building the next generation of the Fediverse. Speak freely.", + "description_limit": 5000, + "email": "alex@alexgleason.me", + "languages": [ + "en" + ], + "max_toot_chars": 5000, + "pleroma": { + "metadata": { + "account_activation_required": false, + "birthday_min_age": 0, + "birthday_required": false, + "features": [ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "v2_suggestions", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "quote_posting", + "media_proxy", + "relay", + "pleroma_emoji_reactions", + "pleroma_chat_messages", + "email_list", + "profile_directory" + ], + "federation": { + "enabled": true, + "exclusions": false, + "mrf_hashtag": { + "federated_timeline_removal": [], + "reject": [], + "sensitive": [ + "nsfw" + ] + }, + "mrf_policies": [ + "TagPolicy", + "SimplePolicy", + "InlineQuotePolicy", + "HashtagPolicy" + ], + "mrf_simple": { + "accept": [], + "avatar_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "banner_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "federated_timeline_removal": [], + "followers_only": [], + "media_nsfw": [], + "media_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "reject": [ + "solagg.com" + ], + "reject_deletes": [], + "report_removal": [] + }, + "mrf_simple_info": {}, + "quarantined_instances": [], + "quarantined_instances_info": { + "quarantined_instances": {} + } + }, + "fields_limits": { + "max_fields": 15, + "max_remote_fields": 20, + "name_length": 512, + "value_length": 2048 + }, + "post_formats": [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ], + "privileged_staff": true + }, + "stats": { + "mau": 71 + }, + "vapid_public_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4" + }, + "poll_limits": { + "max_expiration": 31536000, + "max_option_chars": 200, + "max_options": 20, + "min_expiration": 0 + }, + "registrations": true, + "shout_limit": 5000, + "soapbox": { + "version": "1.1.1" + }, + "stats": { + "domain_count": 8140, + "status_count": 101956, + "user_count": 421 + }, + "thumbnail": "https://media.gleasonator.com/c0d38bde6ef0b3baa483f574797662ebd83ef9e1a1162e8e4fcd930bb4b3c068.png", + "title": "Gleasonator", + "upload_limit": 100000000, + "uri": "https://gleasonator.com", + "urls": { + "streaming_api": "wss://gleasonator.com" + }, + "version": "2.7.2 (compatible; Pleroma 2.4.51-1129-gf2cfef09-gleasonator)" +} diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 44d9d9c95..03139a465 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -297,8 +297,7 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function(dispatch, getState) { if (!isLoggedIn(getState)) return; - const instance = getState().get('instance'); - const { attachmentLimit } = getFeatures(instance); + const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']); const media = getState().getIn(['compose', 'media_attachments']); const progress = new Array(files.length).fill(0); diff --git a/app/soapbox/features/compose/components/poll_form.js b/app/soapbox/features/compose/components/poll_form.js index 6e73cb595..d566b3055 100644 --- a/app/soapbox/features/compose/components/poll_form.js +++ b/app/soapbox/features/compose/components/poll_form.js @@ -199,11 +199,11 @@ class PollForm extends ImmutablePureComponent { } const mapStateToProps = state => { - const pollLimits = state.getIn(['instance', 'poll_limits']); + const pollLimits = state.getIn(['instance', 'configuration', 'polls']); return { maxOptions: pollLimits.get('max_options'), - maxOptionChars: pollLimits.get('max_option_chars'), + maxOptionChars: pollLimits.get('max_characters_per_option'), maxExpiration: pollLimits.get('max_expiration'), minExpiration: pollLimits.get('min_expiration'), }; diff --git a/app/soapbox/features/compose/containers/compose_form_container.js b/app/soapbox/features/compose/containers/compose_form_container.js index 0fe6fc1eb..9baf367a2 100644 --- a/app/soapbox/features/compose/containers/compose_form_container.js +++ b/app/soapbox/features/compose/containers/compose_form_container.js @@ -27,7 +27,7 @@ const mapStateToProps = state => ({ showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, isModalOpen: state.get('modal').modalType === 'COMPOSE', - maxTootChars: state.getIn(['instance', 'max_toot_chars']), + maxTootChars: state.getIn(['instance', 'configuration', 'statuses', 'max_characters']), scheduledAt: state.getIn(['compose', 'schedule']), scheduledStatusCount: state.get('scheduled_statuses').size, }); diff --git a/app/soapbox/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance-test.js index 490df6ba2..17eae2e52 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -1,19 +1,114 @@ import { Map as ImmutableMap } from 'immutable'; +import { INSTANCE_REMEMBER_SUCCESS } from 'soapbox/actions/instance'; + import reducer from '../instance'; describe('instance reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap({ - max_toot_chars: 500, description_limit: 1500, - poll_limits: ImmutableMap({ - max_expiration: 2629746, - max_option_chars: 25, - max_options: 4, - min_expiration: 300, + configuration: ImmutableMap({ + statuses: ImmutableMap({ + max_characters: 500, + max_media_attachments: 4, + }), + polls: ImmutableMap({ + max_options: 4, + max_characters_per_option: 25, + min_expiration: 300, + max_expiration: 2629746, + }), }), version: '0.0.0', })); }); + + describe('INSTANCE_REMEMBER_SUCCESS', () => { + it('normalizes Pleroma instance with Mastodon configuration format', () => { + const action = { + type: INSTANCE_REMEMBER_SUCCESS, + instance: require('soapbox/__fixtures__/pleroma-instance.json'), + }; + + 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.toJS()).toMatchObject(expected); + }); + + it('normalizes Mastodon instance with retained configuration', () => { + const action = { + type: INSTANCE_REMEMBER_SUCCESS, + instance: require('soapbox/__fixtures__/mastodon-instance.json'), + }; + + 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.toJS()).toMatchObject(expected); + }); + + it('normalizes Mastodon 3.0.0 instance with default configuration', () => { + const action = { + type: INSTANCE_REMEMBER_SUCCESS, + instance: require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'), + }; + + 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.toJS()).toMatchObject(expected); + }); + }); }); diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index 8c91b2148..25a04562e 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -4,6 +4,8 @@ import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbo import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; import KVStore from 'soapbox/storage/kv_store'; import { ConfigDB } from 'soapbox/utils/config_db'; +import { parseVersion, PLEROMA } from 'soapbox/utils/features'; +import { isNumber } from 'soapbox/utils/numbers'; import { INSTANCE_REMEMBER_SUCCESS, @@ -28,21 +30,69 @@ const nodeinfoToInstance = nodeinfo => { }); }; -// Set Mastodon defaults, overridden by Pleroma servers +// Use Mastodon defaults const initialState = ImmutableMap({ - max_toot_chars: 500, description_limit: 1500, - poll_limits: ImmutableMap({ - max_expiration: 2629746, - max_option_chars: 25, - max_options: 4, - min_expiration: 300, + configuration: ImmutableMap({ + statuses: ImmutableMap({ + max_characters: 500, + max_media_attachments: 4, + }), + polls: ImmutableMap({ + max_options: 4, + max_characters_per_option: 25, + min_expiration: 300, + max_expiration: 2629746, + }), }), version: '0.0.0', }); +// Build Mastodon configuration from Pleroma instance +const pleromaToMastodonConfig = instance => { + return { + 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']), + }), + }; +}; + +// Use new value only if old value is undefined +const mergeDefined = (oldVal, newVal) => oldVal === undefined ? newVal : oldVal; + +// Get the software's default attachment limit +const getAttachmentLimit = software => software === PLEROMA ? Infinity : 4; + +// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format +const normalizeInstance = instance => { + const { software } = parseVersion(instance.get('version')); + const mastodonConfig = pleromaToMastodonConfig(instance); + + return instance.withMutations(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); + }); + + // Merge defaults & cleanup + instance.mergeDeepWith(mergeDefined, initialState); + instance.deleteAll(['max_toot_chars', 'poll_limits']); + }); +}; + const importInstance = (state, instance) => { - return initialState.mergeDeep(instance); + return normalizeInstance(instance); }; const importNodeinfo = (state, nodeinfo) => { diff --git a/app/soapbox/utils/__tests__/features-test.js b/app/soapbox/utils/__tests__/features-test.js index aec1eb1db..3e722e4ac 100644 --- a/app/soapbox/utils/__tests__/features-test.js +++ b/app/soapbox/utils/__tests__/features-test.js @@ -94,22 +94,6 @@ describe('getFeatures', () => { }); }); - describe('attachmentLimit', () => { - it('is 4 by default', () => { - const instance = ImmutableMap({ version: '3.1.4' }); - const features = getFeatures(instance); - expect(features.attachmentLimit).toEqual(4); - }); - - it('is Infinity for Pleroma', () => { - const instance = ImmutableMap({ - version: '2.7.2 (compatible; Pleroma 1.1.50-42-g3d9ac6ae-develop)', - }); - const features = getFeatures(instance); - expect(features.attachmentLimit).toEqual(Infinity); - }); - }); - describe('focalPoint', () => { it('is true for Mastodon 2.3.0+', () => { const instance = ImmutableMap({ version: '2.3.0' }); diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 1b3ea5369..6df42d1e7 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -49,7 +49,6 @@ export const getFeatures = createSelector([ ]), emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'), emojiReactsRGI: v.software === PLEROMA && gte(v.version, '2.2.49'), - attachmentLimit: v.software === PLEROMA ? Infinity : 4, focalPoint: v.software === MASTODON && gte(v.compatVersion, '2.3.0'), importAPI: v.software === PLEROMA, importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'),