- {this.modeLabel(mode)}
+
+
+ {this.modeLabel(mode)}
+
+
);
@@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent {
modeLabel (mode) {
switch(mode) {
- case 'any': return
;
- case 'all': return
;
- case 'none': return
;
+ case 'any':
+ return
;
+ case 'all':
+ return
;
+ case 'none':
+ return
;
+ default:
+ return '';
}
- return '';
};
render () {
@@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent {
- {this.state.open &&
+
+ {this.state.open && (
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
- }
+ )}
);
}
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js
index c2e026d13..0d3c97a64 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent {
title = () => {
let title = [this.props.params.id];
+
if (this.additionalFor('any')) {
- title.push(' ',
);
+ title.push(' ',
);
}
+
if (this.additionalFor('all')) {
- title.push(' ',
);
+ title.push(' ',
);
}
+
if (this.additionalFor('none')) {
- title.push(' ',
);
+ title.push(' ',
);
}
+
return title;
}
@@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent {
let all = (tags.all || []).map(tag => tag.value);
let none = (tags.none || []).map(tag => tag.value);
- [id, ...any].map((tag) => {
- this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
+ [id, ...any].map(tag => {
+ this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
let tags = status.tags.map(tag => tag.name);
+
return all.filter(tag => tags.includes(tag)).length === all.length &&
none.filter(tag => tags.includes(tag)).length === 0;
})));
@@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent {
const { dispatch } = this.props;
const { id, tags } = this.props.params;
+ this._subscribe(dispatch, id, tags);
dispatch(expandHashtagTimeline(id, { tags }));
}
componentWillReceiveProps (nextProps) {
const { dispatch, params } = this.props;
const { id, tags } = nextProps.params;
+
if (id !== params.id || !isEqual(tags, params.tags)) {
this._unsubscribe();
this._subscribe(dispatch, id, tags);
diff --git a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js
new file mode 100644
index 000000000..3dc59c12e
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+ title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['listEditor', 'title']),
+ disabled: !state.getIn(['listEditor', 'isChanged']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onChange: value => dispatch(changeListEditorTitle(value)),
+ onSubmit: () => dispatch(submitListEditor(false)),
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class ListForm extends React.PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ }
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ }
+
+ handleClick = () => {
+ this.props.onSubmit();
+ }
+
+ render () {
+ const { value, disabled, intl } = this.props;
+
+ const title = intl.formatMessage(messages.title);
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js
index aab0cdd0c..48466604a 100644
--- a/app/javascript/mastodon/features/list_editor/index.js
+++ b/app/javascript/mastodon/features/list_editor/index.js
@@ -7,11 +7,11 @@ import { injectIntl } from 'react-intl';
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
import Account from './components/account';
import Search from './components/search';
+import EditListForm from './components/edit_list_form';
import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
const mapStateToProps = state => ({
- title: state.getIn(['listEditor', 'title']),
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
});
@@ -33,7 +33,6 @@ class ListEditor extends ImmutablePureComponent {
onInitialize: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
- title: PropTypes.string.isRequired,
accountIds: ImmutablePropTypes.list.isRequired,
searchAccountIds: ImmutablePropTypes.list.isRequired,
};
@@ -49,12 +48,12 @@ class ListEditor extends ImmutablePureComponent {
}
render () {
- const { title, accountIds, searchAccountIds, onClear } = this.props;
+ const { accountIds, searchAccountIds, onClear } = this.props;
const showSearch = searchAccountIds.size > 0;
return (
-
{title}
+
diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js
index 02a0dabb1..91e524dd5 100644
--- a/app/javascript/mastodon/reducers/list_editor.js
+++ b/app/javascript/mastodon/reducers/list_editor.js
@@ -22,6 +22,7 @@ import {
const initialState = ImmutableMap({
listId: null,
isSubmitting: false,
+ isChanged: false,
title: '',
accounts: ImmutableMap({
@@ -47,10 +48,16 @@ export default function listEditorReducer(state = initialState, action) {
map.set('isSubmitting', false);
});
case LIST_EDITOR_TITLE_CHANGE:
- return state.set('title', action.value);
+ return state.withMutations(map => {
+ map.set('title', action.value);
+ map.set('isChanged', true);
+ });
case LIST_CREATE_REQUEST:
case LIST_UPDATE_REQUEST:
- return state.set('isSubmitting', true);
+ return state.withMutations(map => {
+ map.set('isSubmitting', true);
+ map.set('isChanged', false);
+ });
case LIST_CREATE_FAIL:
case LIST_UPDATE_FAIL:
return state.set('isSubmitting', false);
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
index 7d8993a50..8429103b8 100644
--- a/app/javascript/styles/contrast/diff.scss
+++ b/app/javascript/styles/contrast/diff.scss
@@ -13,6 +13,10 @@
}
}
+.rich-formatting a,
+.rich-formatting p a,
+.rich-formatting li a,
+.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 78bc2dbb6..de03cf1a6 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -352,6 +352,8 @@
.moved-account-widget,
.memoriam-widget,
.activity-stream,
-.nothing-here {
+.nothing-here,
+.directory__tag > a,
+.directory__tag > div {
box-shadow: none;
}
diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index d5bafe6b6..08806599e 100644
--- a/app/javascript/styles/mastodon/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
@@ -41,3 +41,34 @@
font-size: 16px;
}
}
+
+@mixin search-popout() {
+ background: $simple-background-color;
+ border-radius: 4px;
+ padding: 10px 14px;
+ padding-bottom: 14px;
+ margin-top: 10px;
+ color: $light-text-color;
+ box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+
+ h4 {
+ text-transform: uppercase;
+ color: $light-text-color;
+ font-size: 13px;
+ font-weight: 500;
+ margin-bottom: 10px;
+ }
+
+ li {
+ padding: 4px 0;
+ }
+
+ ul {
+ margin-bottom: 10px;
+ }
+
+ em {
+ font-weight: 500;
+ color: $inverted-text-color;
+ }
+}
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index b6c92a09e..b078d4d24 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -49,15 +49,9 @@ $small-breakpoint: 960px;
}
}
+ strong,
em {
- display: inline;
- margin: 0;
- padding: 0;
font-weight: 700;
- background: transparent;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
color: lighten($darker-text-color, 10%);
}
@@ -796,7 +790,7 @@ $small-breakpoint: 960px;
width: 100%;
display: flex;
flex-direction: row-reverse;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
@@ -846,14 +840,7 @@ $small-breakpoint: 960px;
}
strong {
- display: inline;
- margin: 0;
- padding: 0;
- font-weight: 700;
- background: transparent;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
+ font-weight: 500;
color: lighten($darker-text-color, 10%);
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index e29abf4f3..11823a45b 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -476,7 +476,7 @@
opacity: 0;
transition: opacity .1s ease;
- input {
+ textarea {
background: transparent;
color: $secondary-text-color;
border: 0;
@@ -3056,14 +3056,41 @@ a.status-card.compact:hover {
display: block;
font-weight: 500;
margin-bottom: 10px;
+}
- .column-settings__hashtag-select {
+.column-settings__hashtags {
+ .column-settings__row {
+ margin-bottom: 15px;
+ }
+
+ .column-select {
&__control {
@include search-input();
}
+ &__placeholder {
+ color: $dark-text-color;
+ padding-left: 2px;
+ font-size: 12px;
+ }
+
+ &__value-container {
+ padding-left: 6px;
+ }
+
&__multi-value {
background: lighten($ui-base-color, 8%);
+
+ &__remove {
+ cursor: pointer;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background: lighten($ui-base-color, 12%);
+ color: lighten($darker-text-color, 4%);
+ }
+ }
}
&__multi-value__label,
@@ -3071,9 +3098,42 @@ a.status-card.compact:hover {
color: $darker-text-color;
}
- &__indicator-separator,
+ &__clear-indicator,
&__dropdown-indicator {
- display: none;
+ cursor: pointer;
+ transition: none;
+ color: $dark-text-color;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: lighten($dark-text-color, 4%);
+ }
+ }
+
+ &__indicator-separator {
+ background-color: lighten($ui-base-color, 8%);
+ }
+
+ &__menu {
+ @include search-popout();
+ padding: 0;
+ background: $ui-secondary-color;
+ }
+
+ &__menu-list {
+ padding: 6px;
+ }
+
+ &__option {
+ color: $inverted-text-color;
+ border-radius: 4px;
+ font-size: 14px;
+
+ &--is-focused,
+ &--is-selected {
+ background: darken($ui-secondary-color, 10%);
+ }
}
}
}
@@ -4867,34 +4927,7 @@ a.status-card.compact:hover {
}
.search-popout {
- background: $simple-background-color;
- border-radius: 4px;
- padding: 10px 14px;
- padding-bottom: 14px;
- margin-top: 10px;
- color: $light-text-color;
- box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
-
- h4 {
- text-transform: uppercase;
- color: $light-text-color;
- font-size: 13px;
- font-weight: 500;
- margin-bottom: 10px;
- }
-
- li {
- padding: 4px 0;
- }
-
- ul {
- margin-bottom: 10px;
- }
-
- em {
- font-weight: 500;
- color: $inverted-text-color;
- }
+ @include search-popout();
}
noscript {
@@ -5130,7 +5163,7 @@ noscript {
.icon-button {
flex: 0 0 auto;
- margin-left: 5px;
+ margin: 0 5px;
}
}
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 919678618..7e4e19531 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -4,6 +4,9 @@ class ActivityPub::Activity
include JsonLdHelper
include Redisable
+ SUPPORTED_TYPES = %w(Note).freeze
+ CONVERTED_TYPES = %w(Image Video Article Page).freeze
+
def initialize(json, account, **options)
@json = json
@account = account
@@ -71,6 +74,18 @@ class ActivityPub::Activity
@object_uri ||= value_or_id(@object)
end
+ def unsupported_object_type?
+ @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
+ end
+
+ def supported_object_type?
+ equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
+ end
+
+ def converted_object_type?
+ equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
+ end
+
def distribute(status)
crawl_links(status)
@@ -120,6 +135,23 @@ class ActivityPub::Activity
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
end
+ 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
+ end
+
+ # If the status is not from the actor, try to fetch it
+ return fetch_remote_original_status if value_or_id(first_of_value(@json['attributedTo'])) == @account.uri
+ end
+
def fetch_remote_original_status
if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 34d1b7cbd..04afeea20 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -2,9 +2,7 @@
class ActivityPub::Activity::Announce < ActivityPub::Activity
def perform
- original_status = status_from_uri(object_uri)
- original_status ||= fetch_remote_original_status
-
+ original_status = status_from_object
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
status = Status.find_by(account: @account, reblog: original_status)
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index b49657d4b..1b31768d9 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -1,12 +1,8 @@
# frozen_string_literal: true
class ActivityPub::Activity::Create < ActivityPub::Activity
- SUPPORTED_TYPES = %w(Note).freeze
- CONVERTED_TYPES = %w(Image Video Article Page).freeze
-
def perform
- return if unsupported_object_type? || invalid_origin?(@object['id'])
- return if Tombstone.exists?(uri: @object['id'])
+ return 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?
@@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
end
- def unsupported_object_type?
- @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
- end
-
def unsupported_media_type?(mime_type)
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
end
- def supported_object_type?
- equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
- end
-
- def converted_object_type?
- equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
- end
-
def skip_download?
return @skip_download if defined?(@skip_download)
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
@@ -352,6 +336,37 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
!replied_to_status.nil? && replied_to_status.account.local?
end
+ def related_to_local_activity?
+ fetch? || followed_by_local_accounts? || requested_through_relay? ||
+ 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
+
+ def addresses_local_accounts?
+ return true if @options[:delivered_to_account_id]
+
+ local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
+
+ return false if local_usernames.empty?
+
+ Account.local.where(username: local_usernames).exists?
+ end
+
def forward_for_reply
return unless @json['signature'].present? && reply_to_local?
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
diff --git a/app/models/relay.rb b/app/models/relay.rb
index 7478c110d..6934a5c62 100644
--- a/app/models/relay.rb
+++ b/app/models/relay.rb
@@ -29,6 +29,7 @@ class Relay < ApplicationRecord
payload = Oj.dump(follow_activity(activity_id))
update!(state: :pending, follow_activity_id: activity_id)
+ DeliveryFailureTracker.new(inbox_url).track_success!
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
@@ -37,6 +38,7 @@ class Relay < ApplicationRecord
payload = Oj.dump(unfollow_activity(activity_id))
update!(state: :idle, follow_activity_id: nil)
+ DeliveryFailureTracker.new(inbox_url).track_success!
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb
index 50c4f6a04..b51e8c544 100644
--- a/app/serializers/activitypub/activity_serializer.rb
+++ b/app/serializers/activitypub/activity_serializer.rb
@@ -3,8 +3,8 @@
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
attributes :id, :type, :actor, :published, :to, :cc
- has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce?
- attribute :proper_uri, key: :object, if: :announce?
+ has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce?
+ attribute :proper_uri, key: :object, if: :owned_announce?
attribute :atom_uri, if: :announce?
def id
@@ -42,4 +42,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
def announce?
object.reblog?
end
+
+ def owned_announce?
+ announce? && object.account == object.proper.account && object.proper.private_visibility?
+ end
end
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index 5c54aad89..881df478b 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -44,6 +44,7 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def verify_account!
+ @options[:relayed_through_account] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb
index a8a3ebf0f..a3abe72cf 100644
--- a/app/workers/activitypub/processing_worker.rb
+++ b/app/workers/activitypub/processing_worker.rb
@@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
sidekiq_options backtrace: true
def perform(account_id, body, delivered_to_account_id = nil)
- ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id)
+ ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
end
end
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 35302e37b..28201cc64 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -46,14 +46,14 @@ class Rack::Attack
end
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
- req.api_request? && req.authenticated_user_id
+ req.authenticated_user_id if req.api_request?
end
throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
req.ip if req.api_request?
end
- throttle('throttle_media', limit: 30, period: 30.minutes) do |req|
+ throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
end
@@ -61,6 +61,13 @@ class Rack::Attack
req.ip if req.post? && req.path == '/api/v1/accounts'
end
+ API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
+ API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
+
+ throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
+ req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
+ end
+
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
end
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index d1f11261c..c75d0b643 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -46,7 +46,7 @@ cs:
choices_html: 'Volby uživatele %{name}:'
follow: Sledovat
followers:
- few: Sledovatelé
+ few: Sledující
one: Sledující
other: Sledujících
following: Sledovaných
@@ -618,7 +618,7 @@ cs:
lock_link: Zamkněte svůj účet
purge: Odstranit ze sledujících
success:
- few: V průběhu blokování sledovatelů ze %{count} domén...
+ few: V průběhu blokování sledujících ze %{count} domén...
one: V průběhu blokování sledujících z jedné domény...
other: V průběhu blokování sledujících z %{count} domén...
true_privacy_html: Berte prosím na vědomí, že skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování.
@@ -688,7 +688,7 @@ cs:
body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
mention: "%{name} vás zmínil/a v:"
new_followers_summary:
- few: Navíc jste získal/a %{count} nové sledovatele, zatímco jste byl/a pryč! Skvělé!
+ few: Navíc jste získal/a %{count} nové sledující, zatímco jste byl/a pryč! Skvělé!
one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá!
other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné!
subject:
diff --git a/dist/mastodon-streaming.service b/dist/mastodon-streaming.service
index 5d7c129df..c324fccf4 100644
--- a/dist/mastodon-streaming.service
+++ b/dist/mastodon-streaming.service
@@ -9,7 +9,7 @@ WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
Environment="STREAMING_CLUSTER_NUM=1"
-ExecStart=/usr/bin/npm run start
+ExecStart=/usr/bin/node ./streaming
TimeoutSec=15
Restart=always
diff --git a/public/robots.txt b/public/robots.txt
index 3c9c7c01f..d93648bee 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,5 +1,4 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
-# To ban all spiders from the entire site uncomment the next two lines:
-# User-agent: *
-# Disallow: /
+
+User-agent: *
+Disallow: /media_proxy/
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index 54dd52a60..1725c2843 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Announce do
- let(:sender) { Fabricate(:account) }
+ let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
let(:recipient) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: recipient) }
@@ -11,19 +11,60 @@ RSpec.describe ActivityPub::Activity::Announce do
id: 'foo',
type: 'Announce',
actor: ActivityPub::TagManager.instance.uri_for(sender),
- object: ActivityPub::TagManager.instance.uri_for(status),
+ object: object_json,
}.with_indifferent_access
end
- describe '#perform' do
- subject { described_class.new(json, sender) }
+ subject { described_class.new(json, sender) }
+ before do
+ sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
+ end
+
+ describe '#perform' do
before do
subject.perform
end
- it 'creates a reblog by sender of status' do
- expect(sender.reblogged?(status)).to be true
+ 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
end