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/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..e9c474082b 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' })
%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