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) => {