From 04979486e87720c99ba812bdcae2eb564b7190a0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Jan 2022 14:26:42 -0600 Subject: [PATCH 1/2] Normalize instance configuration for Mastodon --- .../__fixtures__/mastodon-3.0.0-instance.json | 43 ++++++ .../__fixtures__/mastodon-instance.json | 128 +++++++++++++++++ .../__fixtures__/pleroma-instance.json | 131 ++++++++++++++++++ .../features/compose/components/poll_form.js | 4 +- .../containers/compose_form_container.js | 2 +- .../reducers/__tests__/instance-test.js | 104 +++++++++++++- app/soapbox/reducers/instance.js | 54 ++++++-- 7 files changed, 449 insertions(+), 17 deletions(-) create mode 100644 app/soapbox/__fixtures__/mastodon-3.0.0-instance.json create mode 100644 app/soapbox/__fixtures__/mastodon-instance.json create mode 100644 app/soapbox/__fixtures__/pleroma-instance.json 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 project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "description": "Server run by the main developers of the project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "email": "staff@mastodon.social", + "version": "3.4.3", + "urls": { + "streaming_api": "wss://mastodon.social" + }, + "stats": { + "user_count": 619022, + "status_count": 33914684, + "domain_count": 21524 + }, + "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "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 + } + }, + "contact_account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen 🎄", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "

Founder, 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/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..906497926 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -1,19 +1,111 @@ 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, + }), + 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, + }, + 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, + }, + 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..b89ebc4ff 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -28,21 +28,59 @@ 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, + }), + 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; + +// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format +const normalizeInstance = instance => { + const mastodonConfig = pleromaToMastodonConfig(instance); + + return instance.withMutations(instance => { + // Merge configuration + instance.update('configuration', ImmutableMap(), configuration => ( + configuration.mergeDeepWith(mergeDefined, mastodonConfig) + )); + + // 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) => { From e04e75f8314e07c60674674de9254ea57d891bcf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Jan 2022 17:52:12 -0600 Subject: [PATCH 2/2] Set attachment limit from instance --- app/soapbox/actions/compose.js | 3 +-- app/soapbox/reducers/__tests__/instance-test.js | 3 +++ app/soapbox/reducers/instance.js | 12 ++++++++++++ app/soapbox/utils/__tests__/features-test.js | 16 ---------------- app/soapbox/utils/features.js | 1 - 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 4eca355d4..11d53f317 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -296,8 +296,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/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance-test.js index 906497926..17eae2e52 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -11,6 +11,7 @@ describe('instance reducer', () => { configuration: ImmutableMap({ statuses: ImmutableMap({ max_characters: 500, + max_media_attachments: 4, }), polls: ImmutableMap({ max_options: 4, @@ -36,6 +37,7 @@ describe('instance reducer', () => { configuration: { statuses: { max_characters: 5000, + max_media_attachments: Infinity, }, polls: { max_options: 20, @@ -95,6 +97,7 @@ describe('instance reducer', () => { configuration: { statuses: { max_characters: 500, + max_media_attachments: 4, }, polls: { max_options: 4, diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index b89ebc4ff..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, @@ -34,6 +36,7 @@ const initialState = ImmutableMap({ configuration: ImmutableMap({ statuses: ImmutableMap({ max_characters: 500, + max_media_attachments: 4, }), polls: ImmutableMap({ max_options: 4, @@ -63,8 +66,12 @@ const pleromaToMastodonConfig = instance => { // 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 => { @@ -73,6 +80,11 @@ const normalizeInstance = instance => { 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']); 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 fc3dc68ab..fac905130 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'),