diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb index e469c7d210..8b63d0490d 100644 --- a/app/controllers/api/v1/apps/credentials_controller.rb +++ b/app/controllers/api/v1/apps/credentials_controller.rb @@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController respond_to :json def show - render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer + render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) end end diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 7fb84bd1e7..1d3130604c 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -83,7 +83,11 @@ export default class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); + if (me) { + this.props.onReply(this.props.status, this.context.router.history); + } else { + this._openInteractionDialog('reply'); + } } handleShareClick = () => { @@ -94,17 +98,29 @@ export default class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = (e) => { - this.props.onFavourite(this.props.status, e); + if (me) { + this.props.onFavourite(this.props.status, e); + } else { + this._openInteractionDialog('favourite'); + } } handleBookmarkClick = (e) => { this.props.onBookmark(this.props.status, e); } - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); + handleReblogClick = e => { + if (me) { + this.props.onReblog(this.props.status, e); + } else { + this._openInteractionDialog('reblog'); + } } + _openInteractionDialog = type => { + window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -174,7 +190,7 @@ export default class StatusActionBar extends ImmutablePureComponent { const mutingConversation = status.get('muted'); const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - const reblogDisabled = anonymousAccess || (status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']))); + const reblogDisabled = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id'])); const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog; let menu = []; @@ -243,7 +259,6 @@ export default class StatusActionBar extends ImmutablePureComponent { let replyButton = ( {replyButton} - + {shareButton} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 8f49a9a301..120ae68172 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -98,7 +98,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const { expanded, onToggleHidden, settings } = this.props; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; diff --git a/app/javascript/flavours/glitch/packs/error.js b/app/javascript/flavours/glitch/packs/error.js new file mode 100644 index 0000000000..81c86c3abb --- /dev/null +++ b/app/javascript/flavours/glitch/packs/error.js @@ -0,0 +1,13 @@ +import ready from 'flavours/glitch/util/ready'; + +ready(() => { + const image = document.querySelector('img'); + + image.addEventListener('mouseenter', () => { + image.src = '/oops.gif'; + }); + + image.addEventListener('mouseleave', () => { + image.src = '/oops.png'; + }); +}); diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 0c8342c443..d8f3133818 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -7,7 +7,7 @@ pack: filename: packs/common.js stylesheet: true embed: packs/public.js - error: + error: packs/error.js home: filename: packs/home.js preload: diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index bd9fb1dab7..a215b2625e 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -7,7 +7,7 @@ pack: filename: common.js stylesheet: true embed: public.js - error: + error: error.js home: filename: application.js preload: diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 16c7caf1c1..53d17d4180 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -78,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); + if (me) { + this.props.onReply(this.props.status, this.context.router.history); + } else { + this._openInteractionDialog('reply'); + } } handleShareClick = () => { @@ -91,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); + if (me) { + this.props.onFavourite(this.props.status); + } else { + this._openInteractionDialog('favourite'); + } } - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); + handleReblogClick = e => { + if (me) { + this.props.onReblog(this.props.status, e); + } else { + this._openInteractionDialog('reblog'); + } + } + + _openInteractionDialog = type => { + window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } handleDeleteClick = () => { @@ -233,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent { return (
-
{obfuscatedCount(status.get('replies_count'))}
- - +
{obfuscatedCount(status.get('replies_count'))}
+ + {shareButton}
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index ca7ce6f8ea..96a219c947 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['user_lists', 'blocks', 'next']), }); export default @connect(mapStateToProps) @@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, shouldUpdateScroll } = this.props; + const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 5c1bd11610..7c075f5a5c 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -19,6 +19,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ domains: state.getIn(['domain_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']), }); export default @connect(mapStateToProps) @@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, domains: ImmutablePropTypes.orderedSet, intl: PropTypes.object.isRequired, }; @@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains, shouldUpdateScroll } = this.props; + const { intl, domains, shouldUpdateScroll, hasMore } = this.props; if (!domains) { return ( @@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 56ae8764b4..3871e0e5d1 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), + hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), }); export default @connect(mapStateToProps) @@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index f979ef72f9..4ed29a1ce6 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'mutes', 'items']), + hasMore: !!state.getIn(['user_lists', 'mutes', 'next']), }); export default @connect(mapStateToProps) @@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 734353c9bd..49bc43a7ba 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -87,7 +87,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; diff --git a/app/javascript/packs/error.js b/app/javascript/packs/error.js new file mode 100644 index 0000000000..685c890658 --- /dev/null +++ b/app/javascript/packs/error.js @@ -0,0 +1,13 @@ +import ready from '../mastodon/ready'; + +ready(() => { + const image = document.querySelector('img'); + + image.addEventListener('mouseenter', () => { + image.src = '/oops.gif'; + }); + + image.addEventListener('mouseleave', () => { + image.src = '/oops.png'; + }); +}); diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 746def6251..4411ca0b4c 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -100,12 +100,14 @@ body { vertical-align: middle; margin: 20px; - img { - display: block; - max-width: 470px; - width: 100%; - height: auto; - margin-top: -120px; + &__illustration { + img { + display: block; + max-width: 470px; + width: 100%; + height: auto; + margin-top: -120px; + } } h1 { diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 7e4e195313..8265810a00 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -138,11 +138,13 @@ class ActivityPub::Activity def status_from_object # If the status is already known, return it status = status_from_uri(object_uri) + return status unless status.nil? # If the boosted toot is embedded and it is a self-boost, handle it like a Create unless unsupported_object_type? actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri + if actor_id == @account.uri return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform end @@ -166,4 +168,21 @@ class ActivityPub::Activity ensure redis.del(key) end + + def fetch? + !@options[:delivery] + end + + def followed_by_local_accounts? + @account.passive_relationships.exists? + end + + def requested_through_relay? + @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled? + end + + def reject_payload! + Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}") + nil + end end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 04afeea202..9f8ffd9fb7 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -2,8 +2,11 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity def perform + return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity? + original_status = status_from_object - return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status) + + return reject_payload! if original_status.nil? || !announceable?(original_status) status = Status.find_by(account: @account, reblog: original_status) @@ -39,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity def announceable?(status) status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end + + def related_to_local_activity? + followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status? + end + + def reblog_of_local_status? + status_from_uri(object_uri)&.account&.local? + end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 1b31768d96..d7bd65c806 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -2,7 +2,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def perform - return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity? + return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @@ -341,18 +341,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity responds_to_followed_account? || addresses_local_accounts? end - def fetch? - !@options[:delivery] - end - - def followed_by_local_accounts? - @account.passive_relationships.exists? - end - - def requested_through_relay? - @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled? - end - def responds_to_followed_account? !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?) end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index a9316cd4b7..ab68219ade 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -2,7 +2,7 @@ class REST::ApplicationSerializer < ActiveModel::Serializer attributes :id, :name, :website, :redirect_uri, - :client_id, :client_secret + :client_id, :client_secret, :vapid_key def id object.id.to_s @@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer def website object.website.presence end + + def vapid_key + Rails.configuration.x.vapid_public_key + end end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index cab05e60a7..41ed1995d0 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :uri, :title, :description, :email, :version, :urls, :stats, :thumbnail, :max_toot_chars, - :languages + :languages, :registrations has_one :contact_account, serializer: REST::AccountSerializer @@ -55,6 +55,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer [I18n.default_locale] end + def registrations + Setting.open_registrations && !Rails.configuration.x.single_user_mode + end + private def instance_presenter diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml index d662d85e2e..f8315afb56 100644 --- a/app/views/layouts/error.html.haml +++ b/app/views/layouts/error.html.haml @@ -5,10 +5,12 @@ %meta{ charset: 'utf-8' }/ %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ') %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/ + = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous' = render partial: 'layouts/theme', object: (@core || { pack: 'common' }) - = render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' }) + = render partial: 'layouts/theme', object: (@theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } }) %body.error .dialog - %img{ alt: Setting.default_settings['site_title'], src: current_user&.setting_auto_play_gif ? '/oops.gif' : '/oops.png' }/ - %div + .dialog__illustration + %img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/ + .dialog__message %h1= yield :content diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 1725c2843b..94b9d348d0 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -22,11 +22,59 @@ RSpec.describe ActivityPub::Activity::Announce do end describe '#perform' do - before do - subject.perform + context 'when sender is followed by a local account' do + before do + Fabricate(:account).follow!(sender) + subject.perform + end + + context 'a known status' do + let(:object_json) do + ActivityPub::TagManager.instance.uri_for(status) + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(status)).to be true + end + end + + context 'self-boost of a previously unknown status with missing attributedTo' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(sender.statuses.first)).to be true + end + end + + context 'self-boost of a previously unknown status with correct attributedTo' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(sender.statuses.first)).to be true + end + end end - context 'a known status' do + context 'when the status belongs to a local user' do + before do + subject.perform + end + let(:object_json) do ActivityPub::TagManager.instance.uri_for(status) end @@ -36,34 +84,68 @@ RSpec.describe ActivityPub::Activity::Announce do end end - context 'self-boost of a previously unknown status with missing attributedTo' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: 'http://example.com/followers', - } + context 'when the sender is relayed' do + let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') } + let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') } + + subject { described_class.new(json, sender, relayed_through_account: relay_account) } + + context 'and the relay is enabled' do + before do + relay.update(state: :accepted) + subject.perform + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.statuses.count).to eq 2 + end end - it 'creates a reblog by sender of status' do - expect(sender.reblogged?(sender.statuses.first)).to be true + context 'and the relay is disabled' do + before do + subject.perform + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 + end end end - context 'self-boost of a previously unknown status with correct attributedTo' do + context 'when the sender has no relevance to local activity' do + before do + subject.perform + end + let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), to: 'http://example.com/followers', } end - it 'creates a reblog by sender of status' do - expect(sender.reblogged?(sender.statuses.first)).to be true + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 end end end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index cd20b7c7cb..26cb84871c 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do }.with_indifferent_access end - subject { described_class.new(json, sender) } - before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) @@ -23,11 +21,402 @@ RSpec.describe ActivityPub::Activity::Create do end describe '#perform' do - before do - subject.perform + context 'when fetching' do + subject { described_class.new(json, sender) } + + before do + subject.perform + end + + context 'standalone' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + end + + it 'missing to/cc defaults to direct privacy' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'direct' + end + end + + context 'public' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'public' + end + end + + context 'unlisted' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + cc: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'unlisted' + end + end + + context 'private' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'private' + end + end + + context 'limited' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: ActivityPub::TagManager.instance.uri_for(recipient), + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'limited' + end + + it 'creates silent mention' do + status = sender.statuses.first + expect(status.mentions.first).to be_silent + end + end + + context 'direct' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: ActivityPub::TagManager.instance.uri_for(recipient), + tag: { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'direct' + end + end + + context 'as a reply' do + let(:original_status) { Fabricate(:status) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.thread).to eq original_status + expect(status.reply?).to be true + expect(status.in_reply_to_account).to eq original_status.account + expect(status.conversation).to eq original_status.conversation + end + end + + context 'with mentions' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.mentions.map(&:account)).to include(recipient) + end + end + + context 'with mentions missing href' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Mention', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with media attachments' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + url: 'http://example.com/attachment.png', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') + end + end + + context 'with media attachments with focal points' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + url: 'http://example.com/attachment.png', + focalPoint: [0.5, -0.7], + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7') + end + end + + context 'with media attachments missing url' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with hashtags' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Hashtag', + href: 'http://example.com/blah', + name: '#test', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.tags.map(&:name)).to include('test') + end + end + + context 'with hashtags missing name' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Hashtag', + href: 'http://example.com/blah', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with emojis' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + icon: { + url: 'http://example.com/emoji.png', + }, + name: 'tinking', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.emojis.map(&:shortcode)).to include('tinking') + end + end + + context 'with emojis missing name' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + icon: { + url: 'http://example.com/emoji.png', + }, + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with emojis missing icon' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + name: 'tinking', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end end - context 'standalone' do + context 'when sender is followed by local users' do + subject { described_class.new(json, sender, delivery: true) } + + before do + Fabricate(:account).follow!(sender) + subject.perform + end + let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -42,78 +431,23 @@ RSpec.describe ActivityPub::Activity::Create do expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end - - it 'missing to/cc defaults to direct privacy' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'direct' - end end - context 'public' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: 'https://www.w3.org/ns/activitystreams#Public', - } + context 'when sender replies to local status' do + let!(:local_status) { Fabricate(:status) } + + subject { described_class.new(json, sender, delivery: true) } + + before do + subject.perform end - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'public' - end - end - - context 'unlisted' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - cc: 'https://www.w3.org/ns/activitystreams#Public', - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'unlisted' - end - end - - context 'private' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: 'http://example.com/followers', - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'private' - end - end - - context 'limited' do - let(:recipient) { Fabricate(:account) } - let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - to: ActivityPub::TagManager.instance.uri_for(recipient), + inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status), } end @@ -121,28 +455,25 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.visibility).to eq 'limited' - end - - it 'creates silent mention' do - status = sender.statuses.first - expect(status.mentions.first).to be_silent + expect(status.text).to eq 'Lorem ipsum' end end - context 'direct' do - let(:recipient) { Fabricate(:account) } + context 'when sender targets a local user' do + let!(:local_account) { Fabricate(:account) } + + subject { described_class.new(json, sender, delivery: true) } + + before do + subject.perform + end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - to: ActivityPub::TagManager.instance.uri_for(recipient), - tag: { - type: 'Mention', - href: ActivityPub::TagManager.instance.uri_for(recipient), - }, + to: ActivityPub::TagManager.instance.uri_for(local_account), } end @@ -150,19 +481,25 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.visibility).to eq 'direct' + expect(status.text).to eq 'Lorem ipsum' end end - context 'as a reply' do - let(:original_status) { Fabricate(:status) } + context 'when sender cc\'s a local user' do + let!(:local_account) { Fabricate(:account) } + + subject { described_class.new(json, sender, delivery: true) } + + before do + subject.perform + end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + cc: ActivityPub::TagManager.instance.uri_for(local_account), } end @@ -170,240 +507,27 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.thread).to eq original_status - expect(status.reply?).to be true - expect(status.in_reply_to_account).to eq original_status.account - expect(status.conversation).to eq original_status.conversation + expect(status.text).to eq 'Lorem ipsum' end end - context 'with mentions' do - let(:recipient) { Fabricate(:account) } + context 'when the sender has no relevance to local activity' do + subject { described_class.new(json, sender, delivery: true) } + + before do + subject.perform + end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - tag: [ - { - type: 'Mention', - href: ActivityPub::TagManager.instance.uri_for(recipient), - }, - ], } end - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.mentions.map(&:account)).to include(recipient) - end - end - - context 'with mentions missing href' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Mention', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with media attachments' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - url: 'http://example.com/attachment.png', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') - end - end - - context 'with media attachments with focal points' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - url: 'http://example.com/attachment.png', - focalPoint: [0.5, -0.7], - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7') - end - end - - context 'with media attachments missing url' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with hashtags' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Hashtag', - href: 'http://example.com/blah', - name: '#test', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.tags.map(&:name)).to include('test') - end - end - - context 'with hashtags missing name' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Hashtag', - href: 'http://example.com/blah', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with emojis' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - icon: { - url: 'http://example.com/emoji.png', - }, - name: 'tinking', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.emojis.map(&:shortcode)).to include('tinking') - end - end - - context 'with emojis missing name' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - icon: { - url: 'http://example.com/emoji.png', - }, - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with emojis missing icon' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - name: 'tinking', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 end end end