From 526b4b3677dfc5139289b405185024c85bba14dc Mon Sep 17 00:00:00 2001
From: trwnh
Date: Thu, 15 Sep 2022 08:35:06 -0500
Subject: Fix breaking change in admin account API (#19176)
* Fix breaking change in admin account API
Ensure that `ip` is a String value and not returning a raw database entry
* please rubocop
---
app/serializers/rest/admin/account_serializer.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'app/serializers')
diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb
index 3480e8c5a..2fbc7b1cb 100644
--- a/app/serializers/rest/admin/account_serializer.rb
+++ b/app/serializers/rest/admin/account_serializer.rb
@@ -77,6 +77,6 @@ class REST::Admin::AccountSerializer < ActiveModel::Serializer
end
def ip
- ips&.first
+ ips&.first&.ip
end
end
--
cgit
From 50948b46aabc0756d85bc6641f0bd3bcc09bf7d4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Tue, 20 Sep 2022 23:51:21 +0200
Subject: Add ability to filter followed accounts' posts by language (#19095)
---
app/controllers/api/v1/accounts_controller.rb | 6 +-
.../mastodon/features/account/components/header.js | 5 +
.../features/account_timeline/components/header.js | 6 +
.../containers/header_container.js | 8 +-
.../features/subscribed_languages_modal/index.js | 121 +++++++++++++++++++++
.../mastodon/features/ui/components/modal_root.js | 2 +
.../mastodon/locales/defaultMessages.json | 25 +++++
app/javascript/mastodon/locales/en.json | 4 +
app/lib/feed_manager.rb | 2 +
app/models/concerns/account_interactions.rb | 23 ++--
app/models/export.rb | 4 +-
app/models/follow.rb | 4 +-
app/models/follow_request.rb | 4 +-
app/serializers/rest/relationship_serializer.rb | 7 +-
app/services/follow_service.rb | 13 ++-
app/services/import_service.rb | 2 +-
app/validators/language_validator.rb | 21 ++++
app/workers/refollow_worker.rb | 7 +-
app/workers/unfollow_follow_worker.rb | 9 +-
.../20220829192633_add_languages_to_follows.rb | 5 +
...20829192658_add_languages_to_follow_requests.rb | 5 +
db/schema.rb | 4 +-
.../controllers/api/v1/accounts_controller_spec.rb | 11 ++
.../exports/following_accounts_controller_spec.rb | 2 +-
spec/lib/feed_manager_spec.rb | 12 ++
spec/models/concerns/account_interactions_spec.rb | 2 +-
spec/models/export_spec.rb | 4 +-
spec/models/follow_request_spec.rb | 2 +-
spec/services/follow_service_spec.rb | 13 +++
spec/workers/refollow_worker_spec.rb | 4 +-
30 files changed, 298 insertions(+), 39 deletions(-)
create mode 100644 app/javascript/mastodon/features/subscribed_languages_modal/index.js
create mode 100644 app/validators/language_validator.rb
create mode 100644 db/migrate/20220829192633_add_languages_to_follows.rb
create mode 100644 db/migrate/20220829192658_add_languages_to_follow_requests.rb
(limited to 'app/serializers')
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 5537cc9b0..be84720aa 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -30,12 +30,12 @@ class Api::V1::AccountsController < Api::BaseController
self.response_body = Oj.dump(response.body)
self.status = response.status
rescue ActiveRecord::RecordInvalid => e
- render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
+ render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity
end
def follow
- follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
- options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
+ follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true)
+ options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options)
end
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 1ad9341c7..8f2753c35 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -51,6 +51,7 @@ const messages = defineMessages({
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+ languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
});
const dateFormatOptions = {
@@ -85,6 +86,7 @@ class Header extends ImmutablePureComponent {
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
+ onChangeLanguages: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
@@ -212,6 +214,9 @@ class Header extends ImmutablePureComponent {
} else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
+
+ menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
+ menu.push(null);
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index fab0bc597..f9838442f 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -22,6 +22,7 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
+ onChangeLanguages: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
@@ -91,6 +92,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onEditAccountNote(this.props.account);
}
+ handleChangeLanguages = () => {
+ this.props.onChangeLanguages(this.props.account);
+ }
+
render () {
const { account, hidden, hideTabs } = this.props;
@@ -117,6 +122,7 @@ export default class Header extends ImmutablePureComponent {
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote}
+ onChangeLanguages={this.handleChangeLanguages}
domain={this.props.domain}
hidden={hidden}
/>
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 371794dd7..3d6eb487d 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -121,12 +121,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockDomain(domain));
},
- onAddToList(account){
+ onAddToList (account) {
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
+ onChangeLanguages (account) {
+ dispatch(openModal('SUBSCRIBED_LANGUAGES', {
+ accountId: account.get('id'),
+ }));
+ },
+
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/mastodon/features/subscribed_languages_modal/index.js b/app/javascript/mastodon/features/subscribed_languages_modal/index.js
new file mode 100644
index 000000000..6a1bb2c47
--- /dev/null
+++ b/app/javascript/mastodon/features/subscribed_languages_modal/index.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
+import { languages as preloadedLanguages } from 'mastodon/initial_state';
+import Option from 'mastodon/features/report/components/option';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import IconButton from 'mastodon/components/icon_button';
+import Button from 'mastodon/components/button';
+import { followAccount } from 'mastodon/actions/accounts';
+
+const messages = defineMessages({
+ close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+const getAccountLanguages = createSelector([
+ (state, accountId) => state.getIn(['timelines', `account:${accountId}`, 'items'], ImmutableList()),
+ state => state.get('statuses'),
+], (statusIds, statuses) =>
+ new ImmutableSet(statusIds.map(statusId => statuses.get(statusId)).filter(status => !status.get('reblog')).map(status => status.get('language'))));
+
+const mapStateToProps = (state, { accountId }) => ({
+ acct: state.getIn(['accounts', accountId, 'acct']),
+ availableLanguages: getAccountLanguages(state, accountId),
+ selectedLanguages: ImmutableSet(state.getIn(['relationships', accountId, 'languages']) || ImmutableList()),
+});
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+
+ onSubmit (languages) {
+ dispatch(followAccount(accountId, { languages }));
+ },
+
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class SubscribedLanguagesModal extends ImmutablePureComponent {
+
+ static propTypes = {
+ accountId: PropTypes.string.isRequired,
+ acct: PropTypes.string.isRequired,
+ availableLanguages: ImmutablePropTypes.setOf(PropTypes.string),
+ selectedLanguages: ImmutablePropTypes.setOf(PropTypes.string),
+ onClose: PropTypes.func.isRequired,
+ languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
+ intl: PropTypes.object.isRequired,
+ submit: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ languages: preloadedLanguages,
+ };
+
+ state = {
+ selectedLanguages: this.props.selectedLanguages,
+ };
+
+ handleLanguageToggle = (value, checked) => {
+ const { selectedLanguages } = this.state;
+
+ if (checked) {
+ this.setState({ selectedLanguages: selectedLanguages.add(value) });
+ } else {
+ this.setState({ selectedLanguages: selectedLanguages.delete(value) });
+ }
+ };
+
+ handleSubmit = () => {
+ this.props.onSubmit(this.state.selectedLanguages.toArray());
+ this.props.onClose();
+ }
+
+ renderItem (value) {
+ const language = this.props.languages.find(language => language[0] === value);
+ const checked = this.state.selectedLanguages.includes(value);
+
+ return (
+
+ );
+ }
+
+ render () {
+ const { acct, availableLanguages, selectedLanguages, intl, onClose } = this.props;
+
+ return (
+
+
+
+ {acct} }} />
+
+
+
+
+
+
+ {availableLanguages.union(selectedLanguages).map(value => this.renderItem(value))}
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index b2c30e079..dfa89f2ce 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -11,6 +11,7 @@ import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import AudioModal from './audio_modal';
import ConfirmationModal from './confirmation_modal';
+import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal';
import FocalPointModal from './focal_point_modal';
import {
MuteModal,
@@ -39,6 +40,7 @@ const MODAL_COMPONENTS = {
'LIST_ADDER': ListAdder,
'COMPARE_HISTORY': CompareHistoryModal,
'FILTER': FilterModal,
+ 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
};
export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 13ef56922..4c208c3cb 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1030,6 +1030,10 @@
"defaultMessage": "Open moderation interface for @{name}",
"id": "status.admin_account"
},
+ {
+ "defaultMessage": "Change subscribed languages",
+ "id": "account.languages"
+ },
{
"defaultMessage": "Follows you",
"id": "account.follows_you"
@@ -3350,6 +3354,27 @@
],
"path": "app/javascript/mastodon/features/status/index.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Close",
+ "id": "lightbox.close"
+ },
+ {
+ "defaultMessage": "Change subscribed languages for {target}",
+ "id": "subscribed_languages.target"
+ },
+ {
+ "defaultMessage": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
+ "id": "subscribed_languages.lead"
+ },
+ {
+ "defaultMessage": "Save changes",
+ "id": "subscribed_languages.save"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/subscribed_languages_modal/index.json"
+ },
{
"descriptors": [
{
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 521bc4455..4f515b321 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -24,6 +24,7 @@
"account.follows_you": "Follows you",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.joined": "Joined {date}",
+ "account.languages": "Change subscribed languages",
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
@@ -522,6 +523,9 @@
"status.uncached_media_warning": "Not available",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
+ "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
+ "subscribed_languages.save": "Save changes",
+ "subscribed_languages.target": "Change subscribed languages for {target}",
"suggestions.dismiss": "Dismiss suggestion",
"suggestions.header": "You might be interested in…",
"tabs_bar.federated_timeline": "Federated",
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index ccff2667b..f2d204a64 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -354,6 +354,7 @@ class FeedManager
def filter_from_home?(status, receiver_id, crutches)
return false if receiver_id == status.account_id
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
+ return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
check_for_blocks = crutches[:active_mentions][status.id] || []
check_for_blocks.concat([status.account_id])
@@ -542,6 +543,7 @@ class FeedManager
end
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true)
+ crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 9b358d338..15c49f2fe 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -9,6 +9,7 @@ module AccountInteractions
mapping[follow.target_account_id] = {
reblogs: follow.show_reblogs?,
notify: follow.notify?,
+ languages: follow.languages,
}
end
end
@@ -38,6 +39,7 @@ module AccountInteractions
mapping[follow_request.target_account_id] = {
reblogs: follow_request.show_reblogs?,
notify: follow_request.notify?,
+ languages: follow_request.languages,
}
end
end
@@ -100,12 +102,13 @@ module AccountInteractions
has_many :announcement_mutes, dependent: :destroy
end
- def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
- rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
+ def follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
+ rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, languages: languages, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
.find_or_create_by!(target_account: other_account)
- rel.show_reblogs = reblogs unless reblogs.nil?
- rel.notify = notify unless notify.nil?
+ rel.show_reblogs = reblogs unless reblogs.nil?
+ rel.notify = notify unless notify.nil?
+ rel.languages = languages unless languages.nil?
rel.save! if rel.changed?
@@ -114,12 +117,13 @@ module AccountInteractions
rel
end
- def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
- rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
+ def request_follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
+ rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, languages: languages, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
.find_or_create_by!(target_account: other_account)
- rel.show_reblogs = reblogs unless reblogs.nil?
- rel.notify = notify unless notify.nil?
+ rel.show_reblogs = reblogs unless reblogs.nil?
+ rel.notify = notify unless notify.nil?
+ rel.languages = languages unless languages.nil?
rel.save! if rel.changed?
@@ -288,8 +292,7 @@ module AccountInteractions
private
- def remove_potential_friendship(other_account, mutual = false)
+ def remove_potential_friendship(other_account)
PotentialFriendshipTracker.remove(id, other_account.id)
- PotentialFriendshipTracker.remove(other_account.id, id) if mutual
end
end
diff --git a/app/models/export.rb b/app/models/export.rb
index 5216eed5e..2457dcc15 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -30,9 +30,9 @@ class Export
end
def to_following_accounts_csv
- CSV.generate(headers: ['Account address', 'Show boosts'], write_headers: true) do |csv|
+ CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv|
account.active_relationships.includes(:target_account).reorder(id: :desc).each do |follow|
- csv << [acct(follow.target_account), follow.show_reblogs]
+ csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')]
end
end
end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index a5e3fe809..e5cecbbc1 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -11,6 +11,7 @@
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
+# languages :string is an Array
#
class Follow < ApplicationRecord
@@ -27,6 +28,7 @@ class Follow < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
validates :account_id, uniqueness: { scope: :target_account_id }
+ validates :languages, language: true
scope :recent, -> { reorder(id: :desc) }
@@ -35,7 +37,7 @@ class Follow < ApplicationRecord
end
def revoke_request!
- FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
+ FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, languages: languages, uri: uri)
destroy!
end
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 0b6f7629a..9034250c0 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -11,6 +11,7 @@
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
+# languages :string is an Array
#
class FollowRequest < ApplicationRecord
@@ -27,9 +28,10 @@ class FollowRequest < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
validates :account_id, uniqueness: { scope: :target_account_id }
+ validates :languages, language: true
def authorize!
- account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri, bypass_limit: true)
+ account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true)
MergeWorker.perform_async(target_account.id, account.id) if account.local?
destroy!
end
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index afd4cddf9..31fc60eb2 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class REST::RelationshipSerializer < ActiveModel::Serializer
- attributes :id, :following, :showing_reblogs, :notifying, :followed_by,
+ attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
:domain_blocking, :endorsed, :note
@@ -25,6 +25,11 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
false
end
+ def languages
+ (instance_options[:relationships].following[object.id] || {})[:languages] ||
+ (instance_options[:relationships].requested[object.id] || {})[:languages]
+ end
+
def followed_by
instance_options[:relationships].followed_by[object.id] || false
end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index ed28e1371..feea40e3c 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -11,6 +11,7 @@ class FollowService < BaseService
# @param [Hash] options
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
+ # @option [Array] :languages Which languages to allow on the home feed from this account, defaults to all
# @option [Boolean] :bypass_locked
# @option [Boolean] :bypass_limit Allow following past the total follow number
# @option [Boolean] :with_rate_limit
@@ -57,15 +58,15 @@ class FollowService < BaseService
end
def change_follow_options!
- @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
+ @source_account.follow!(@target_account, **follow_options)
end
def change_follow_request_options!
- @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
+ @source_account.request_follow!(@target_account, **follow_options)
end
def request_follow!
- follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
+ follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
@@ -77,7 +78,7 @@ class FollowService < BaseService
end
def direct_follow!
- follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
+ follow = @source_account.follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow')
MergeWorker.perform_async(@target_account.id, @source_account.id)
@@ -88,4 +89,8 @@ class FollowService < BaseService
def build_json(follow_request)
Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
end
+
+ def follow_options
+ @options.slice(:reblogs, :notify, :languages)
+ end
end
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
index 8e6640b9d..676c37bde 100644
--- a/app/services/import_service.rb
+++ b/app/services/import_service.rb
@@ -27,7 +27,7 @@ class ImportService < BaseService
def import_follows!
parse_import_data!(['Account address'])
- import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true })
+ import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true }, notify: { header: 'Notify on new posts', default: false }, languages: { header: 'Languages', default: nil })
end
def import_blocks!
diff --git a/app/validators/language_validator.rb b/app/validators/language_validator.rb
new file mode 100644
index 000000000..b723e1a40
--- /dev/null
+++ b/app/validators/language_validator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class LanguageValidator < ActiveModel::EachValidator
+ include LanguagesHelper
+
+ def validate_each(record, attribute, value)
+ record.errors.add(attribute, :invalid) unless valid?(value)
+ end
+
+ private
+
+ def valid?(str)
+ if str.nil?
+ true
+ elsif str.is_a?(Array)
+ str.all? { |x| valid_locale?(x) }
+ else
+ valid_locale?(str)
+ end
+ end
+end
diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb
index 319b00109..4b712d3aa 100644
--- a/app/workers/refollow_worker.rb
+++ b/app/workers/refollow_worker.rb
@@ -10,8 +10,9 @@ class RefollowWorker
return unless target_account.activitypub?
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
- reblogs = follow.show_reblogs?
- notify = follow.notify?
+ reblogs = follow.show_reblogs?
+ notify = follow.notify?
+ languages = follow.languages
# Locally unfollow remote account
follower = follow.account
@@ -19,7 +20,7 @@ class RefollowWorker
# Schedule re-follow
begin
- FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify, bypass_limit: true)
+ FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_limit: true)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next
end
diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb
index 0bd5ff472..7203b4888 100644
--- a/app/workers/unfollow_follow_worker.rb
+++ b/app/workers/unfollow_follow_worker.rb
@@ -10,11 +10,12 @@ class UnfollowFollowWorker
old_target_account = Account.find(old_target_account_id)
new_target_account = Account.find(new_target_account_id)
- follow = follower_account.active_relationships.find_by(target_account: old_target_account)
- reblogs = follow&.show_reblogs?
- notify = follow&.notify?
+ follow = follower_account.active_relationships.find_by(target_account: old_target_account)
+ reblogs = follow&.show_reblogs?
+ notify = follow&.notify?
+ languages = follow&.languages
- FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked, bypass_limit: true)
+ FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
true
diff --git a/db/migrate/20220829192633_add_languages_to_follows.rb b/db/migrate/20220829192633_add_languages_to_follows.rb
new file mode 100644
index 000000000..f6cf48880
--- /dev/null
+++ b/db/migrate/20220829192633_add_languages_to_follows.rb
@@ -0,0 +1,5 @@
+class AddLanguagesToFollows < ActiveRecord::Migration[6.1]
+ def change
+ add_column :follows, :languages, :string, array: true
+ end
+end
diff --git a/db/migrate/20220829192658_add_languages_to_follow_requests.rb b/db/migrate/20220829192658_add_languages_to_follow_requests.rb
new file mode 100644
index 000000000..f98fabb22
--- /dev/null
+++ b/db/migrate/20220829192658_add_languages_to_follow_requests.rb
@@ -0,0 +1,5 @@
+class AddLanguagesToFollowRequests < ActiveRecord::Migration[6.1]
+ def change
+ add_column :follow_requests, :languages, :string, array: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index db22f538a..1a98b22db 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_08_27_195229) do
+ActiveRecord::Schema.define(version: 2022_08_29_192658) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -461,6 +461,7 @@ ActiveRecord::Schema.define(version: 2022_08_27_195229) do
t.boolean "show_reblogs", default: true, null: false
t.string "uri"
t.boolean "notify", default: false, null: false
+ t.string "languages", array: true
t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
end
@@ -472,6 +473,7 @@ ActiveRecord::Schema.define(version: 2022_08_27_195229) do
t.boolean "show_reblogs", default: true, null: false
t.string "uri"
t.boolean "notify", default: false, null: false
+ t.string "languages", array: true
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 5d5c245c5..d6bbcefd7 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -145,6 +145,17 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
expect(json[:showing_reblogs]).to be false
expect(json[:notifying]).to be true
end
+
+ it 'changes languages option' do
+ post :follow, params: { id: other_account.id, languages: %w(en es) }
+
+ json = body_as_json
+
+ expect(json[:following]).to be true
+ expect(json[:showing_reblogs]).to be false
+ expect(json[:notifying]).to be false
+ expect(json[:languages]).to match_array %w(en es)
+ end
end
end
diff --git a/spec/controllers/settings/exports/following_accounts_controller_spec.rb b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
index 78858e772..bfe010555 100644
--- a/spec/controllers/settings/exports/following_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
@@ -11,7 +11,7 @@ describe Settings::Exports::FollowingAccountsController do
sign_in user, scope: :user
get :index, format: :csv
- expect(response.body).to eq "Account address,Show boosts\nusername@domain,true\n"
+ expect(response.body).to eq "Account address,Show boosts,Notify on new posts,Languages\nusername@domain,true,false,\n"
end
end
end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 48c57b86e..0f3b05e5a 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -127,6 +127,18 @@ RSpec.describe FeedManager do
reblog = Fabricate(:status, reblog: status, account: jeff)
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
end
+
+ it 'returns true for German post when follow is set to English only' do
+ alice.follow!(bob, languages: %w(en))
+ status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
+ expect(FeedManager.instance.filter?(:home, status, alice)).to be true
+ end
+
+ it 'returns false for German post when follow is set to German' do
+ alice.follow!(bob, languages: %w(de))
+ status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
+ expect(FeedManager.instance.filter?(:home, status, alice)).to be false
+ end
end
context 'for mentions feed' do
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 0369aff10..1d1898ab0 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -14,7 +14,7 @@ describe AccountInteractions do
context 'account with Follow' do
it 'returns { target_account_id => true }' do
Fabricate(:follow, account: account, target_account: target_account)
- is_expected.to eq(target_account_id => { reblogs: true, notify: false })
+ is_expected.to eq(target_account_id => { reblogs: true, notify: false, languages: nil })
end
end
diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb
index 4e6b824bb..135d7a36b 100644
--- a/spec/models/export_spec.rb
+++ b/spec/models/export_spec.rb
@@ -35,8 +35,8 @@ describe Export do
results = export.strip.split("\n")
expect(results.size).to eq 3
- expect(results.first).to eq 'Account address,Show boosts'
- expect(results.second).to eq 'one@local.host,true'
+ expect(results.first).to eq 'Account address,Show boosts,Notify on new posts,Languages'
+ expect(results.second).to eq 'one@local.host,true,false,'
end
end
diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb
index 36ce8ee60..901eabc9d 100644
--- a/spec/models/follow_request_spec.rb
+++ b/spec/models/follow_request_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do
let(:target_account) { Fabricate(:account) }
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
- expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, bypass_limit: true)
+ expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true)
expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
expect(follow_request).to receive(:destroy!)
follow_request.authorize!
diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb
index 02bc87c58..88346ec54 100644
--- a/spec/services/follow_service_spec.rb
+++ b/spec/services/follow_service_spec.rb
@@ -121,6 +121,19 @@ RSpec.describe FollowService, type: :service do
expect(sender.muting_reblogs?(bob)).to be false
end
end
+
+ describe 'already followed account, changing languages' do
+ let(:bob) { Fabricate(:account, username: 'bob') }
+
+ before do
+ sender.follow!(bob)
+ subject.call(sender, bob, languages: %w(en es))
+ end
+
+ it 'changes languages' do
+ expect(Follow.find_by(account: sender, target_account: bob)&.languages).to match_array %w(en es)
+ end
+ end
end
context 'remote ActivityPub account' do
diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb
index df6731b64..d9c2293b6 100644
--- a/spec/workers/refollow_worker_spec.rb
+++ b/spec/workers/refollow_worker_spec.rb
@@ -23,8 +23,8 @@ describe RefollowWorker do
result = subject.perform(account.id)
expect(result).to be_nil
- expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false, bypass_limit: true)
- expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false, bypass_limit: true)
+ expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false, languages: nil, bypass_limit: true)
+ expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false, languages: nil, bypass_limit: true)
end
end
end
--
cgit
From 8cf7006d4efbcfdd4a4ab688db1bcc73a2915a47 Mon Sep 17 00:00:00 2001
From: Claire
Date: Wed, 21 Sep 2022 22:45:57 +0200
Subject: Refactor ActivityPub handling to prepare for non-Account actors
(#19212)
* Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService
ActivityPub::FetchRemoteAccountService is kept as a wrapper for when the actor is
specifically required to be an Account
* Refactor SignatureVerification to allow non-Account actors
* fixup! Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService
* Refactor ActivityPub::FetchRemoteKeyService to potentially return non-Account actors
* Refactor inbound ActivityPub payload processing to accept non-Account actors
* Refactor inbound ActivityPub processing to accept activities relayed through non-Account
* Refactor how Account key URIs are built
* Refactor Request and drop unused key_id_format parameter
* Rename ActivityPub::Dereferencer `signature_account` to `signature_actor`
---
app/controllers/accounts_controller.rb | 2 +-
app/controllers/activitypub/claims_controller.rb | 2 +-
.../activitypub/collections_controller.rb | 2 +-
.../followers_synchronizations_controller.rb | 2 +-
app/controllers/activitypub/inboxes_controller.rb | 10 +-
app/controllers/activitypub/outboxes_controller.rb | 2 +-
app/controllers/activitypub/replies_controller.rb | 2 +-
app/controllers/concerns/signature_verification.rb | 52 +++---
app/controllers/follower_accounts_controller.rb | 2 +-
app/controllers/following_accounts_controller.rb | 2 +-
app/controllers/statuses_controller.rb | 2 +-
app/controllers/tags_controller.rb | 2 +-
app/lib/activitypub/activity.rb | 10 +-
app/lib/activitypub/dereferencer.rb | 6 +-
app/lib/activitypub/linked_data_signature.rb | 6 +-
app/lib/activitypub/tag_manager.rb | 8 +
app/lib/request.rb | 18 +--
.../activitypub/public_key_serializer.rb | 2 +-
.../activitypub/fetch_remote_account_service.rb | 78 +--------
.../activitypub/fetch_remote_actor_service.rb | 80 +++++++++
.../activitypub/fetch_remote_key_service.rb | 22 +--
.../activitypub/process_collection_service.rb | 11 +-
app/services/fetch_resource_service.rb | 2 +-
app/services/keys/claim_service.rb | 2 +-
app/services/resolve_url_service.rb | 4 +-
app/workers/activitypub/delivery_worker.rb | 2 +-
app/workers/activitypub/processing_worker.rb | 12 +-
spec/controllers/accounts_controller_spec.rb | 2 +-
.../activitypub/collections_controller_spec.rb | 2 +-
.../followers_synchronizations_controller_spec.rb | 2 +-
.../activitypub/inboxes_controller_spec.rb | 2 +-
.../activitypub/outboxes_controller_spec.rb | 2 +-
.../activitypub/replies_controller_spec.rb | 2 +-
.../concerns/signature_verification_spec.rb | 45 ++++++
spec/controllers/statuses_controller_spec.rb | 2 +-
spec/lib/activitypub/activity/announce_spec.rb | 2 +-
spec/lib/activitypub/dereferencer_spec.rb | 8 +-
spec/lib/activitypub/linked_data_signature_spec.rb | 14 +-
.../activitypub/fetch_remote_actor_service_spec.rb | 180 +++++++++++++++++++++
.../activitypub/process_collection_service_spec.rb | 6 +-
spec/services/fetch_resource_service_spec.rb | 2 +-
41 files changed, 436 insertions(+), 180 deletions(-)
create mode 100644 app/services/activitypub/fetch_remote_actor_service.rb
create mode 100644 spec/services/activitypub/fetch_remote_actor_service_spec.rb
(limited to 'app/serializers')
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index fe7d934dc..d92f91b30 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -7,7 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureAuthentication
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
before_action :set_body_classes
diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb
index 08ad952df..339333e46 100644
--- a/app/controllers/activitypub/claims_controller.rb
+++ b/app/controllers/activitypub/claims_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_claim_result
def create
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index e4e994a98..d94a285ea 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items
before_action :set_size
before_action :set_type
diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb
index 940b77cf0..4e445bcb1 100644
--- a/app/controllers/activitypub/followers_synchronizations_controller.rb
+++ b/app/controllers/activitypub/followers_synchronizations_controller.rb
@@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_items
before_action :set_cache_headers
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index 92dcb5ac7..5ee85474e 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
- before_action :require_signature!
+ before_action :require_actor_signature!
skip_before_action :authenticate_user!
def create
@@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def upgrade_account
- if signed_request_account.ostatus?
+ if signed_request_account&.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
- DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
+ DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
- return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
+ return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
@@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def process_payload
- ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
+ ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
end
end
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index cd3992502..60d201f76 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index 4ff7cfa08..8e0f9de2e 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 4da068aed..2394574b3 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -45,10 +45,14 @@ module SignatureVerification
end
end
- def require_signature!
+ def require_account_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
+ def require_actor_signature!
+ render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
+ end
+
def signed_request?
request.headers['Signature'].present?
end
@@ -68,7 +72,11 @@ module SignatureVerification
end
def signed_request_account
- return @signed_request_account if defined?(@signed_request_account)
+ signed_request_actor.is_a?(Account) ? signed_request_actor : nil
+ end
+
+ def signed_request_actor
+ return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
@@ -78,22 +86,22 @@ module SignatureVerification
verify_signature_strength!
verify_body_digest!
- account = account_from_key_id(signature_params['keyId'])
+ actor = actor_from_key_id(signature_params['keyId'])
- raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
+ raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
- return account unless verify_signature(account, signature, compare_signed_string).nil?
+ return actor unless verify_signature(actor, signature, compare_signed_string).nil?
- account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
+ actor = stoplight_wrap_request { actor_refresh_key!(actor) }
- raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
+ raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
- return account unless verify_signature(account, signature, compare_signed_string).nil?
+ return actor unless verify_signature(actor, signature, compare_signed_string).nil?
- fail_with! "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
+ fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
rescue SignatureVerificationError => e
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
@@ -112,7 +120,7 @@ module SignatureVerification
def fail_with!(message)
@signature_verification_failure_reason = message
- @signed_request_account = nil
+ @signed_request_actor = nil
end
def signature_params
@@ -160,10 +168,10 @@ module SignatureVerification
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
- def verify_signature(account, signature, compare_signed_string)
- if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
- @signed_request_account = account
- @signed_request_account
+ def verify_signature(actor, signature, compare_signed_string)
+ if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
+ @signed_request_actor = actor
+ @signed_request_actor
end
rescue OpenSSL::PKey::RSAError
nil
@@ -226,7 +234,7 @@ module SignatureVerification
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
- def account_from_key_id(key_id)
+ def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@@ -237,13 +245,13 @@ module SignatureVerification
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
- account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
+ account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
- rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
@@ -255,12 +263,14 @@ module SignatureVerification
.run
end
- def account_refresh_key(account)
- return if account.local? || !account.activitypub?
- ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true, suppress_errors: false)
+ def actor_refresh_key!(actor)
+ return if actor.local? || !actor.activitypub?
+ return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
+
+ ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
- rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, Webfinger::Error => e
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index f3f8336c9..da7bb4ed2 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 69f0321f8..c37e3b68c 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index c52170d08..7d9db4d5b 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -8,7 +8,7 @@ class StatusesController < ApplicationController
layout 'public'
- before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index b82da8f0c..6dbc2667a 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -8,7 +8,7 @@ class TagsController < ApplicationController
layout 'public'
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 7ff06ea39..f4c67cccd 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -116,12 +116,12 @@ class ActivityPub::Activity
def dereference_object!
return unless @object.is_a?(String)
- dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
+ dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
@object = dereferencer.object unless dereferencer.object.nil?
end
- def signed_fetch_account
+ def signed_fetch_actor
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
first_mentioned_local_account || first_local_follower
@@ -163,15 +163,15 @@ class ActivityPub::Activity
end
def followed_by_local_accounts?
- @account.passive_relationships.exists? || @options[:relayed_through_account]&.passive_relationships&.exists?
+ @account.passive_relationships.exists? || (@options[:relayed_through_actor].is_a?(Account) && @options[:relayed_through_actor].passive_relationships&.exists?)
end
def requested_through_relay?
- @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
+ @options[:relayed_through_actor] && Relay.find_by(inbox_url: @options[:relayed_through_actor].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}"}")
+ Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
nil
end
end
diff --git a/app/lib/activitypub/dereferencer.rb b/app/lib/activitypub/dereferencer.rb
index bea69608f..4d7756d71 100644
--- a/app/lib/activitypub/dereferencer.rb
+++ b/app/lib/activitypub/dereferencer.rb
@@ -3,10 +3,10 @@
class ActivityPub::Dereferencer
include JsonLdHelper
- def initialize(uri, permitted_origin: nil, signature_account: nil)
+ def initialize(uri, permitted_origin: nil, signature_actor: nil)
@uri = uri
@permitted_origin = permitted_origin
- @signature_account = signature_account
+ @signature_actor = signature_actor
end
def object
@@ -46,7 +46,7 @@ class ActivityPub::Dereferencer
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
req.add_headers(headers) if headers
- req.on_behalf_of(@signature_account) if @signature_account
+ req.on_behalf_of(@signature_actor) if @signature_actor
req.perform do |res|
if res.code == 200
diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb
index e853a970e..f90adaf6c 100644
--- a/app/lib/activitypub/linked_data_signature.rb
+++ b/app/lib/activitypub/linked_data_signature.rb
@@ -9,7 +9,7 @@ class ActivityPub::LinkedDataSignature
@json = json.with_indifferent_access
end
- def verify_account!
+ def verify_actor!
return unless @json['signature'].is_a?(Hash)
type = @json['signature']['type']
@@ -18,7 +18,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
- creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
+ creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
return if creator.nil?
@@ -35,7 +35,7 @@ class ActivityPub::LinkedDataSignature
def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
- 'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
+ 'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
'created' => Time.now.utc.iso8601,
}
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index f6b9741fa..3d6b28ef5 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -44,6 +44,10 @@ class ActivityPub::TagManager
end
end
+ def key_uri_for(target)
+ [uri_for(target), '#main-key'].join
+ end
+
def uri_for_username(username)
account_url(username: username)
end
@@ -155,6 +159,10 @@ class ActivityPub::TagManager
path_params[param]
end
+ def uri_to_actor(uri)
+ uri_to_resource(uri, Account)
+ end
+
def uri_to_resource(uri, klass)
return if uri.nil?
diff --git a/app/lib/request.rb b/app/lib/request.rb
index eac04c798..648aa3085 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -40,12 +40,11 @@ class Request
set_digest! if options.key?(:body)
end
- def on_behalf_of(account, key_id_format = :uri, sign_with: nil)
- raise ArgumentError, 'account must not be nil' if account.nil?
+ def on_behalf_of(actor, sign_with: nil)
+ raise ArgumentError, 'actor must not be nil' if actor.nil?
- @account = account
- @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
- @key_id_format = key_id_format
+ @actor = actor
+ @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
self
end
@@ -79,7 +78,7 @@ class Request
end
def headers
- (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
+ (@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
@@ -128,12 +127,7 @@ class Request
end
def key_id
- case @key_id_format
- when :acct
- @account.to_webfinger_s
- when :uri
- [ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
- end
+ ActivityPub::TagManager.instance.key_uri_for(@actor)
end
def http_client
diff --git a/app/serializers/activitypub/public_key_serializer.rb b/app/serializers/activitypub/public_key_serializer.rb
index 62ed49e81..8621517e7 100644
--- a/app/serializers/activitypub/public_key_serializer.rb
+++ b/app/serializers/activitypub/public_key_serializer.rb
@@ -6,7 +6,7 @@ class ActivityPub::PublicKeySerializer < ActivityPub::Serializer
attributes :id, :owner, :public_key_pem
def id
- [ActivityPub::TagManager.instance.uri_for(object), '#main-key'].join
+ ActivityPub::TagManager.instance.key_uri_for(object)
end
def owner
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index d7d739c59..ca7a8c6ca 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -1,80 +1,12 @@
# frozen_string_literal: true
-class ActivityPub::FetchRemoteAccountService < BaseService
- include JsonLdHelper
- include DomainControlHelper
- include WebfingerHelper
-
- class Error < StandardError; end
-
- SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
-
+class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
- return if domain_not_allowed?(uri)
- return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
-
- @json = begin
- if prefetched_body.nil?
- fetch_resource(uri, id)
- else
- body_to_json(prefetched_body, compare_id: id ? uri : nil)
- end
- rescue Oj::ParseError
- raise Error, "Error parsing JSON-LD document #{uri}"
- end
-
- raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
- raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
- raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
- raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
-
- @uri = @json['id']
- @username = @json['preferredUsername']
- @domain = Addressable::URI.parse(@uri).normalized_host
-
- check_webfinger! unless only_key
-
- ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
- rescue Error => e
- Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
- raise unless suppress_errors
- end
-
- private
-
- def check_webfinger!
- webfinger = webfinger!("acct:#{@username}@#{@domain}")
- confirmed_username, confirmed_domain = split_acct(webfinger.subject)
-
- if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
- raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
- return
- end
-
- webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
- @username, @domain = split_acct(webfinger.subject)
-
- unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
- raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
- end
-
- raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
- rescue Webfinger::RedirectError => e
- raise Error, e.message
- rescue Webfinger::Error => e
- raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
- end
-
- def split_acct(acct)
- acct.gsub(/\Aacct:/, '').split('@')
- end
-
- def supported_context?
- super(@json)
- end
+ actor = super
+ return actor if actor.nil? || actor.is_a?(Account)
- def expected_type?
- equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
+ Rails.logger.debug "Fetching account #{uri} failed: Expected Account, got #{actor.class.name}"
+ raise Error, "Expected Account, got #{actor.class.name}" unless suppress_errors
end
end
diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb
new file mode 100644
index 000000000..17bf2f287
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_actor_service.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemoteActorService < BaseService
+ include JsonLdHelper
+ include DomainControlHelper
+ include WebfingerHelper
+
+ class Error < StandardError; end
+
+ SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
+
+ # Does a WebFinger roundtrip on each call, unless `only_key` is true
+ def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
+ return if domain_not_allowed?(uri)
+ return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
+
+ @json = begin
+ if prefetched_body.nil?
+ fetch_resource(uri, id)
+ else
+ body_to_json(prefetched_body, compare_id: id ? uri : nil)
+ end
+ rescue Oj::ParseError
+ raise Error, "Error parsing JSON-LD document #{uri}"
+ end
+
+ raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
+ raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
+ raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
+ raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
+
+ @uri = @json['id']
+ @username = @json['preferredUsername']
+ @domain = Addressable::URI.parse(@uri).normalized_host
+
+ check_webfinger! unless only_key
+
+ ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
+ rescue Error => e
+ Rails.logger.debug "Fetching actor #{uri} failed: #{e.message}"
+ raise unless suppress_errors
+ end
+
+ private
+
+ def check_webfinger!
+ webfinger = webfinger!("acct:#{@username}@#{@domain}")
+ confirmed_username, confirmed_domain = split_acct(webfinger.subject)
+
+ if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ return
+ end
+
+ webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
+ @username, @domain = split_acct(webfinger.subject)
+
+ unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
+ raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
+ end
+
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ rescue Webfinger::RedirectError => e
+ raise Error, e.message
+ rescue Webfinger::Error => e
+ raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
+ end
+
+ def split_acct(acct)
+ acct.gsub(/\Aacct:/, '').split('@')
+ end
+
+ def supported_context?
+ super(@json)
+ end
+
+ def expected_type?
+ equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
+ end
+end
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
index 01008d883..fe8f60b55 100644
--- a/app/services/activitypub/fetch_remote_key_service.rb
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -5,7 +5,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
class Error < StandardError; end
- # Returns account that owns the key
+ # Returns actor that owns the key
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?
@@ -27,7 +27,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
- return find_account(@json['id'], @json, suppress_errors) if person?
+ return find_actor(@json['id'], @json, suppress_errors) if person?
@owner = fetch_resource(owner_uri, true)
@@ -36,7 +36,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
- find_account(owner_uri, @owner, suppress_errors)
+ find_actor(owner_uri, @owner, suppress_errors)
rescue Error => e
Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
raise unless suppress_errors
@@ -44,18 +44,18 @@ class ActivityPub::FetchRemoteKeyService < BaseService
private
- def find_account(uri, prefetched_body, suppress_errors)
- account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
- account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
- account
+ def find_actor(uri, prefetched_body, suppress_errors)
+ actor = ActivityPub::TagManager.instance.uri_to_actor(uri)
+ actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
+ actor
end
def expected_type?
- person? || public_key?
+ actor? || public_key?
end
- def person?
- equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
+ def actor?
+ equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def public_key?
@@ -67,7 +67,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
end
def expected_owner_type?
- equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
+ equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def confirmed_owner?
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index eb008c40a..fffe30195 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -3,8 +3,8 @@
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
- def call(body, account, **options)
- @account = account
+ def call(body, actor, **options)
+ @account = actor
@json = original_json = Oj.load(body, mode: :strict)
@options = options
@@ -16,6 +16,7 @@ class ActivityPub::ProcessCollectionService < BaseService
end
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
+ return unless @account.is_a?(Account)
if @json['signature'].present?
# We have verified the signature, but in the compaction step above, might
@@ -66,8 +67,10 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def verify_account!
- @options[:relayed_through_account] = @account
- @account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
+ @options[:relayed_through_actor] = @account
+ @account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
+ @account = nil unless @account.is_a?(Account)
+ @account
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
nil
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index 6c0093cd4..73204e55d 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -47,7 +47,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
- [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
+ [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal
link_header = response['Link'] && parse_link_header(response)
diff --git a/app/services/keys/claim_service.rb b/app/services/keys/claim_service.rb
index 69568a0d1..ae9e24a24 100644
--- a/app/services/keys/claim_service.rb
+++ b/app/services/keys/claim_service.rb
@@ -72,7 +72,7 @@ class Keys::ClaimService < BaseService
def build_post_request(uri)
Request.new(:post, uri).tap do |request|
- request.on_behalf_of(@source_account, :uri)
+ request.on_behalf_of(@source_account)
request.add_headers(HEADERS)
end
end
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index e2c745673..37c856cf8 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -20,8 +20,8 @@ class ResolveURLService < BaseService
private
def process_url
- if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
- ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
+ if equals_or_includes_any?(type, ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
+ ActivityPub::FetchRemoteActorService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 788f2cf80..d9153132b 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -37,7 +37,7 @@ class ActivityPub::DeliveryWorker
def build_request(http_client)
Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request|
- request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
+ request.on_behalf_of(@source_account, sign_with: @options[:sign_with])
request.add_headers(HEADERS)
request.add_headers({ 'Collection-Synchronization' => synchronization_header }) if ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] != 'true' && @options[:synchronize_followers]
end
diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb
index 37e316354..4d06ad079 100644
--- a/app/workers/activitypub/processing_worker.rb
+++ b/app/workers/activitypub/processing_worker.rb
@@ -5,11 +5,15 @@ class ActivityPub::ProcessingWorker
sidekiq_options backtrace: true, retry: 8
- def perform(account_id, body, delivered_to_account_id = nil)
- account = Account.find_by(id: account_id)
- return if account.nil?
+ def perform(actor_id, body, delivered_to_account_id = nil, actor_type = 'Account')
+ case actor_type
+ when 'Account'
+ actor = Account.find_by(id: actor_id)
+ end
- ActivityPub::ProcessCollectionService.new.call(body, account, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
+ return if actor.nil?
+
+ ActivityPub::ProcessCollectionService.new.call(body, actor, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug "Error processing incoming ActivityPub object: #{e}"
end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 662a89927..12266c800 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -420,7 +420,7 @@ RSpec.describe AccountsController, type: :controller do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 4d87f80ce..f78d9abbf 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
Fabricate(:status_pin, account: account)
Fabricate(:status_pin, account: account)
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
index e233bd560..c19bb8cae 100644
--- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
+++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
index 973ad83bb..2f023197b 100644
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
let(:remote_account) { nil }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'POST #create' do
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 04f036447..74bf46a5e 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
index a35957f24..aee1a8b1a 100644
--- a/spec/controllers/activitypub/replies_controller_spec.rb
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
before do
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
- allow(controller).to receive(:signed_request_account).and_return(remote_querier)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
Fabricate(:status, thread: status, visibility: :public)
Fabricate(:status, thread: status, visibility: :public)
diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb
index 05fb1445b..6e73643b4 100644
--- a/spec/controllers/concerns/signature_verification_spec.rb
+++ b/spec/controllers/concerns/signature_verification_spec.rb
@@ -3,6 +3,16 @@
require 'rails_helper'
describe ApplicationController, type: :controller do
+ class WrappedActor
+ attr_reader :wrapped_account
+
+ def initialize(wrapped_account)
+ @wrapped_account = wrapped_account
+ end
+
+ delegate :uri, :keypair, to: :wrapped_account
+ end
+
controller do
include SignatureVerification
@@ -73,6 +83,41 @@ describe ApplicationController, type: :controller do
end
end
+ context 'with a valid actor that is not an Account' do
+ let(:actor) { WrappedActor.new(author) }
+
+ before do
+ get :success
+
+ fake_request = Request.new(:get, request.url)
+ fake_request.on_behalf_of(author)
+
+ request.headers.merge!(fake_request.headers)
+
+ allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do
+ actor
+ end
+ end
+
+ describe '#signed_request?' do
+ it 'returns true' do
+ expect(controller.signed_request?).to be true
+ end
+ end
+
+ describe '#signed_request_account' do
+ it 'returns nil' do
+ expect(controller.signed_request_account).to be_nil
+ end
+ end
+
+ describe '#signed_request_actor' do
+ it 'returns the expected actor' do
+ expect(controller.signed_request_actor).to eq actor
+ end
+ end
+ end
+
context 'with request older than a day' do
before do
get :success
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 05fae67fa..6ed5d4bbb 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -426,7 +426,7 @@ describe StatusesController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
context 'when account blocks account' do
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index 41806b258..e9cd6c68c 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -115,7 +115,7 @@ RSpec.describe ActivityPub::Activity::Announce do
let(:object_json) { 'https://example.com/actor/hello-world' }
- subject { described_class.new(json, sender, relayed_through_account: relay_account) }
+ subject { described_class.new(json, sender, relayed_through_actor: relay_account) }
before do
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
diff --git a/spec/lib/activitypub/dereferencer_spec.rb b/spec/lib/activitypub/dereferencer_spec.rb
index ce30513d7..e50b497c7 100644
--- a/spec/lib/activitypub/dereferencer_spec.rb
+++ b/spec/lib/activitypub/dereferencer_spec.rb
@@ -4,10 +4,10 @@ RSpec.describe ActivityPub::Dereferencer do
describe '#object' do
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
let(:permitted_origin) { 'https://example.com' }
- let(:signature_account) { nil }
+ let(:signature_actor) { nil }
let(:uri) { nil }
- subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
+ subject { described_class.new(uri, permitted_origin: permitted_origin, signature_actor: signature_actor).object }
before do
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
@@ -21,7 +21,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
@@ -52,7 +52,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index 2222c46fb..d55a7c7fa 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
stub_jsonld_contexts!
end
- describe '#verify_account!' do
+ describe '#verify_actor!' do
context 'when signature matches' do
let(:raw_signature) do
{
@@ -32,7 +32,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
it 'returns creator' do
- expect(subject.verify_account!).to eq sender
+ expect(subject.verify_actor!).to eq sender
end
end
@@ -40,7 +40,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { nil }
it 'returns nil' do
- expect(subject.verify_account!).to be_nil
+ expect(subject.verify_actor!).to be_nil
end
end
@@ -55,7 +55,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
it 'returns nil' do
- expect(subject.verify_account!).to be_nil
+ expect(subject.verify_actor!).to be_nil
end
end
end
@@ -73,14 +73,14 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
it 'can be verified again' do
- expect(described_class.new(subject).verify_account!).to eq sender
+ expect(described_class.new(subject).verify_actor!).to eq sender
end
end
- def sign(from_account, options, document)
+ def sign(from_actor, options, document)
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
document_hash = Digest::SHA256.hexdigest(canonicalize(document))
to_be_verified = options_hash + document_hash
- Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
+ Base64.strict_encode64(from_actor.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
end
end
diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb
new file mode 100644
index 000000000..20117c66d
--- /dev/null
+++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb
@@ -0,0 +1,180 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
+ subject { ActivityPub::FetchRemoteActorService.new }
+
+ let!(:actor) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/alice',
+ type: 'Person',
+ preferredUsername: 'alice',
+ name: 'Alice',
+ summary: 'Foo bar',
+ inbox: 'http://example.com/alice/inbox',
+ }
+ end
+
+ describe '#call' do
+ let(:account) { subject.call('https://example.com/alice', id: true) }
+
+ shared_examples 'sets profile data' do
+ it 'returns an account' do
+ expect(account).to be_an Account
+ end
+
+ it 'sets display name' do
+ expect(account.display_name).to eq 'Alice'
+ end
+
+ it 'sets note' do
+ expect(account.note).to eq 'Foo bar'
+ end
+
+ it 'sets URL' do
+ expect(account.url).to eq 'https://example.com/alice'
+ end
+ end
+
+ context 'when the account does not have a inbox' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ actor[:inbox] = nil
+
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'returns nil' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'when URI and WebFinger share the same host' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'sets username and domain from webfinger' do
+ expect(account.username).to eq 'alice'
+ expect(account.domain).to eq 'example.com'
+ end
+
+ include_examples 'sets profile data'
+ end
+
+ context 'when WebFinger presents different domain than URI' do
+ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'looks up "redirected" webfinger' do
+ account
+ expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+ end
+
+ it 'sets username and domain from final webfinger' do
+ expect(account.username).to eq 'alice'
+ expect(account.domain).to eq 'iscool.af'
+ end
+
+ include_examples 'sets profile data'
+ end
+
+ context 'when WebFinger returns a different URI' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'when WebFinger returns a different URI after a redirection' do
+ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'looks up "redirected" webfinger' do
+ account
+ expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'with wrong id' do
+ it 'does not create account' do
+ expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index 3eccaab5b..093a188a2 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
it 'does not process payload if no signature exists' do
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)
@@ -77,7 +77,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'processes payload with actor if valid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor)
expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
subject.call(json, forwarder)
@@ -86,7 +86,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'does not process payload if invalid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)
diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb
index ded05ffbc..c0c96ab69 100644
--- a/spec/services/fetch_resource_service_spec.rb
+++ b/spec/services/fetch_resource_service_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe FetchResourceService, type: :service do
it 'signs request' do
subject
- expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made
+ expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.key_uri_for(Account.representative))}"/ })).to have_been_made
end
context 'when content type is application/atom+xml' do
--
cgit
From 0d6b878808a02aa4a544e894f06419c0f612c163 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Fri, 23 Sep 2022 23:00:12 +0200
Subject: Add user content translations with configurable backends (#19218)
---
.../api/v1/statuses/translations_controller.rb | 29 ++++++++++++
app/javascript/mastodon/actions/statuses.js | 39 +++++++++++++++-
app/javascript/mastodon/components/status.js | 16 ++++++-
.../mastodon/components/status_content.js | 31 +++++++++----
.../mastodon/containers/status_container.js | 10 ++++
.../features/status/components/detailed_status.js | 13 +++++-
app/javascript/mastodon/features/status/index.js | 13 ++++++
app/javascript/mastodon/reducers/statuses.js | 6 +++
app/lib/translation_service.rb | 23 ++++++++++
app/lib/translation_service/deepl.rb | 53 ++++++++++++++++++++++
app/lib/translation_service/libre_translate.rb | 43 ++++++++++++++++++
app/lib/translation_service/translation.rb | 5 ++
app/serializers/rest/translation_serializer.rb | 9 ++++
app/services/translate_status_service.rb | 24 ++++++++++
config/initializers/inflections.rb | 1 +
config/routes.rb | 2 +
16 files changed, 306 insertions(+), 11 deletions(-)
create mode 100644 app/controllers/api/v1/statuses/translations_controller.rb
create mode 100644 app/lib/translation_service.rb
create mode 100644 app/lib/translation_service/deepl.rb
create mode 100644 app/lib/translation_service/libre_translate.rb
create mode 100644 app/lib/translation_service/translation.rb
create mode 100644 app/serializers/rest/translation_serializer.rb
create mode 100644 app/services/translate_status_service.rb
(limited to 'app/serializers')
diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
new file mode 100644
index 000000000..540b17d00
--- /dev/null
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::TranslationsController < Api::BaseController
+ include Authorization
+
+ before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
+ before_action :set_status
+ before_action :set_translation
+
+ rescue_from TranslationService::NotConfiguredError, with: :not_found
+ rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
+
+ def create
+ render json: @translation, serializer: REST::TranslationSerializer
+ end
+
+ private
+
+ def set_status
+ @status = Status.find(params[:status_id])
+ authorize @status, :show?
+ rescue Mastodon::NotPermittedError
+ not_found
+ end
+
+ def set_translation
+ @translation = TranslateStatusService.new.call(@status, content_locale)
+ end
+end
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 32a4f1f85..4ae1b21e0 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -34,6 +34,11 @@ export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
+export const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
+export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
+export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
+export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
+
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@@ -309,4 +314,36 @@ export function toggleStatusCollapse(id, isCollapsed) {
id,
isCollapsed,
};
-}
+};
+
+export const translateStatus = id => (dispatch, getState) => {
+ dispatch(translateStatusRequest(id));
+
+ api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
+ dispatch(translateStatusSuccess(id, response.data));
+ }).catch(error => {
+ dispatch(translateStatusFail(id, error));
+ });
+};
+
+export const translateStatusRequest = id => ({
+ type: STATUS_TRANSLATE_REQUEST,
+ id,
+});
+
+export const translateStatusSuccess = (id, translation) => ({
+ type: STATUS_TRANSLATE_SUCCESS,
+ id,
+ translation,
+});
+
+export const translateStatusFail = (id, error) => ({
+ type: STATUS_TRANSLATE_FAIL,
+ id,
+ error,
+});
+
+export const undoStatusTranslation = id => ({
+ type: STATUS_TRANSLATE_UNDO,
+ id,
+});
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 6fc132bf5..0d3b51f07 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -85,6 +85,7 @@ class Status extends ImmutablePureComponent {
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func,
+ onTranslate: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
@@ -171,6 +172,10 @@ class Status extends ImmutablePureComponent {
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
}
+ handleTranslate = () => {
+ this.props.onTranslate(this._properStatus());
+ }
+
renderLoadingMediaGallery () {
return ;
}
@@ -512,7 +517,16 @@ class Status extends ImmutablePureComponent {
-
+
{media}
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 724165ada..c8f7bc095 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -1,7 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
@@ -10,7 +10,8 @@ import { autoPlayGif } from 'mastodon/initial_state';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
-export default class StatusContent extends React.PureComponent {
+export default @injectIntl
+class StatusContent extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -21,9 +22,11 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool,
showThread: PropTypes.bool,
onExpandedToggle: PropTypes.func,
+ onTranslate: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
+ intl: PropTypes.object,
};
state = {
@@ -163,20 +166,26 @@ export default class StatusContent extends React.PureComponent {
}
}
+ handleTranslate = () => {
+ this.props.onTranslate();
+ }
+
setRef = (c) => {
this.node = c;
}
render () {
- const { status } = this.props;
+ const { status, intl } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
+ const renderTranslate = this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && intl.locale !== status.get('language');
+ const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
- const content = { __html: status.get('contentHtml') };
+ const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
- const lang = status.get('language');
+ const lang = status.get('translation') ? intl.locale : status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
@@ -195,6 +204,12 @@ export default class StatusContent extends React.PureComponent {
);
+ const translateButton = (
+
+ );
+
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
@@ -223,7 +238,7 @@ export default class StatusContent extends React.PureComponent {
{!hidden && !!status.get('poll') && }
-
+ {!hidden && renderTranslate && translateButton}
{renderViewThread && showThreadButton}
);
@@ -233,7 +248,7 @@ export default class StatusContent extends React.PureComponent {
{!!status.get('poll') && }
-
+ {renderTranslate && translateButton}
{renderViewThread && showThreadButton}
,
];
@@ -249,7 +264,7 @@ export default class StatusContent extends React.PureComponent {
{!!status.get('poll') && }
-
+ {renderTranslate && translateButton}
{renderViewThread && showThreadButton}
);
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 28698b082..9280a6ee3 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -25,6 +25,8 @@ import {
revealStatus,
toggleStatusCollapse,
editStatus,
+ translateStatus,
+ undoStatusTranslation,
} from '../actions/statuses';
import {
unmuteAccount,
@@ -150,6 +152,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(editStatus(status.get('id'), history));
},
+ onTranslate (status) {
+ if (status.get('translation')) {
+ dispatch(undoStatusTranslation(status.get('id')));
+ } else {
+ dispatch(translateStatus(status.get('id')));
+ }
+ },
+
onDirect (account, router) {
dispatch(directCompose(account, router));
},
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 5c43c2038..320a847f7 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -37,6 +37,7 @@ class DetailedStatus extends ImmutablePureComponent {
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired,
+ onTranslate: PropTypes.func.isRequired,
measureHeight: PropTypes.bool,
onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired,
@@ -103,6 +104,11 @@ class DetailedStatus extends ImmutablePureComponent {
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
+ handleTranslate = () => {
+ const { onTranslate, status } = this.props;
+ onTranslate(status);
+ }
+
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
@@ -260,7 +266,12 @@ class DetailedStatus extends ImmutablePureComponent {
-
+
{media}
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 4d7f24834..5ff7e060e 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -32,6 +32,8 @@ import {
editStatus,
hideStatus,
revealStatus,
+ translateStatus,
+ undoStatusTranslation,
} from '../../actions/statuses';
import {
unblockAccount,
@@ -339,6 +341,16 @@ class Status extends ImmutablePureComponent {
}
}
+ handleTranslate = status => {
+ const { dispatch } = this.props;
+
+ if (status.get('translation')) {
+ dispatch(undoStatusTranslation(status.get('id')));
+ } else {
+ dispatch(translateStatus(status.get('id')));
+ }
+ }
+
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
@@ -558,6 +570,7 @@ class Status extends ImmutablePureComponent {
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden}
+ onTranslate={this.handleTranslate}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 53dec9585..7efb49d85 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -13,6 +13,8 @@ import {
STATUS_REVEAL,
STATUS_HIDE,
STATUS_COLLAPSE,
+ STATUS_TRANSLATE_SUCCESS,
+ STATUS_TRANSLATE_UNDO,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@@ -77,6 +79,10 @@ export default function statuses(state = initialState, action) {
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
+ case STATUS_TRANSLATE_SUCCESS:
+ return state.setIn([action.id, 'translation'], fromJS(action.translation));
+ case STATUS_TRANSLATE_UNDO:
+ return state.deleteIn([action.id, 'translation']);
default:
return state;
}
diff --git a/app/lib/translation_service.rb b/app/lib/translation_service.rb
new file mode 100644
index 000000000..526e26ae5
--- /dev/null
+++ b/app/lib/translation_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class TranslationService
+ class Error < StandardError; end
+ class NotConfiguredError < Error; end
+ class TooManyRequestsError < Error; end
+ class QuotaExceededError < Error; end
+ class UnexpectedResponseError < Error; end
+
+ def self.configured
+ if ENV['DEEPL_API_KEY'].present?
+ TranslationService::DeepL.new(ENV.fetch('DEEPL_PLAN', 'free'), ENV['DEEPL_API_KEY'])
+ elsif ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
+ TranslationService::LibreTranslate.new(ENV['LIBRE_TRANSLATE_ENDPOINT'], ENV['LIBRE_TRANSLATE_API_KEY'])
+ else
+ raise NotConfiguredError
+ end
+ end
+
+ def translate(_text, _source_language, _target_language)
+ raise NotImplementedError
+ end
+end
diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb
new file mode 100644
index 000000000..89ccf01e5
--- /dev/null
+++ b/app/lib/translation_service/deepl.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class TranslationService::DeepL < TranslationService
+ include JsonLdHelper
+
+ def initialize(plan, api_key)
+ super()
+
+ @plan = plan
+ @api_key = api_key
+ end
+
+ def translate(text, source_language, target_language)
+ request(text, source_language, target_language).perform do |res|
+ case res.code
+ when 429
+ raise TooManyRequestsError
+ when 456
+ raise QuotaExceededError
+ when 200...300
+ transform_response(res.body_with_limit)
+ else
+ raise UnexpectedResponseError
+ end
+ end
+ end
+
+ private
+
+ def request(text, source_language, target_language)
+ req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language.upcase, target_lang: target_language, tag_handling: 'html' })
+ req.add_headers('Authorization': "DeepL-Auth-Key #{@api_key}")
+ req
+ end
+
+ def endpoint_url
+ if @plan == 'free'
+ 'https://api-free.deepl.com/v2/translate'
+ else
+ 'https://api.deepl.com/v2/translate'
+ end
+ end
+
+ def transform_response(str)
+ json = Oj.load(str, mode: :strict)
+
+ raise UnexpectedResponseError unless json.is_a?(Hash)
+
+ Translation.new(text: json.dig('translations', 0, 'text'), detected_source_language: json.dig('translations', 0, 'detected_source_language')&.downcase)
+ rescue Oj::ParseError
+ raise UnexpectedResponseError
+ end
+end
diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb
new file mode 100644
index 000000000..66acdeed7
--- /dev/null
+++ b/app/lib/translation_service/libre_translate.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class TranslationService::LibreTranslate < TranslationService
+ def initialize(base_url, api_key)
+ super()
+
+ @base_url = base_url
+ @api_key = api_key
+ end
+
+ def translate(text, source_language, target_language)
+ request(text, source_language, target_language).perform do |res|
+ case res.code
+ when 429
+ raise TooManyRequestsError
+ when 403
+ raise QuotaExceededError
+ when 200...300
+ transform_response(res.body_with_limit, source_language)
+ else
+ raise UnexpectedResponseError
+ end
+ end
+ end
+
+ private
+
+ def request(text, source_language, target_language)
+ req = Request.new(:post, "#{@base_url}/translate", body: Oj.dump(q: text, source: source_language, target: target_language, format: 'html', api_key: @api_key))
+ req.add_headers('Content-Type': 'application/json')
+ req
+ end
+
+ def transform_response(str, source_language)
+ json = Oj.load(str, mode: :strict)
+
+ raise UnexpectedResponseError unless json.is_a?(Hash)
+
+ Translation.new(text: json['translatedText'], detected_source_language: source_language)
+ rescue Oj::ParseError
+ raise UnexpectedResponseError
+ end
+end
diff --git a/app/lib/translation_service/translation.rb b/app/lib/translation_service/translation.rb
new file mode 100644
index 000000000..a55b82574
--- /dev/null
+++ b/app/lib/translation_service/translation.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class TranslationService::Translation < ActiveModelSerializers::Model
+ attributes :text, :detected_source_language
+end
diff --git a/app/serializers/rest/translation_serializer.rb b/app/serializers/rest/translation_serializer.rb
new file mode 100644
index 000000000..a06f23f32
--- /dev/null
+++ b/app/serializers/rest/translation_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class REST::TranslationSerializer < ActiveModel::Serializer
+ attributes :content, :detected_source_language
+
+ def content
+ object.text
+ end
+end
diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb
new file mode 100644
index 000000000..b375226be
--- /dev/null
+++ b/app/services/translate_status_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class TranslateStatusService < BaseService
+ CACHE_TTL = 1.day.freeze
+
+ def call(status, target_language)
+ raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility?
+
+ @status = status
+ @target_language = target_language
+
+ Rails.cache.fetch("translations/#{@status.language}/#{@target_language}/#{content_hash}", expires_in: CACHE_TTL) { translation_backend.translate(@status.text, @status.language, @target_language) }
+ end
+
+ private
+
+ def translation_backend
+ TranslationService.configured
+ end
+
+ def content_hash
+ Digest::SHA256.base64digest(@status.text)
+ end
+end
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 3e5a55617..a361cb0ec 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -25,6 +25,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'REST'
inflect.acronym 'URL'
inflect.acronym 'ASCII'
+ inflect.acronym 'DeepL'
inflect.singular 'data', 'data'
end
diff --git a/config/routes.rb b/config/routes.rb
index 13a4a1618..9491c5177 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -393,6 +393,8 @@ Rails.application.routes.draw do
resource :history, only: :show
resource :source, only: :show
+
+ post :translate, to: 'translations#create'
end
member do
--
cgit
From 43b5d5e38d2b8ad8f1d1ad0911c3c1718159c912 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Thu, 29 Sep 2022 04:39:33 +0200
Subject: Add logged-out access to the web UI (#18961)
---
app/controllers/home_controller.rb | 18 ++---
app/javascript/mastodon/actions/accounts.js | 6 +-
app/javascript/mastodon/actions/markers.js | 10 +--
app/javascript/mastodon/components/logo.js | 4 +-
app/javascript/mastodon/containers/mastodon.js | 2 +-
.../mastodon/features/account/components/header.js | 30 +++++---
.../mastodon/features/community_timeline/index.js | 6 ++
.../mastodon/features/directory/index.js | 6 ++
app/javascript/mastodon/features/explore/index.js | 6 ++
app/javascript/mastodon/features/explore/links.js | 11 +++
.../mastodon/features/explore/results.js | 18 ++++-
.../mastodon/features/explore/suggestions.js | 11 +++
app/javascript/mastodon/features/explore/tags.js | 11 +++
.../mastodon/features/hashtag_timeline/index.js | 18 ++++-
.../mastodon/features/public_timeline/index.js | 6 ++
app/javascript/mastodon/features/status/index.js | 24 ++++++-
.../features/ui/components/columns_area.js | 4 +-
.../features/ui/components/compose_panel.js | 22 +++++-
.../features/ui/components/document_title.js | 41 -----------
.../mastodon/features/ui/components/link_footer.js | 46 +++++++++---
.../features/ui/components/navigation_panel.js | 83 ++++++++++++++++------
.../features/ui/components/sign_in_banner.js | 11 +++
app/javascript/mastodon/features/ui/index.js | 43 ++++++++---
app/javascript/mastodon/initial_state.js | 2 +
app/javascript/styles/mastodon/_mixins.scss | 1 +
app/javascript/styles/mastodon/components.scss | 56 +++++++++++++--
app/lib/permalink_redirector.rb | 4 --
app/serializers/initial_state_serializer.rb | 11 ++-
app/views/home/index.html.haml | 12 ++--
package.json | 1 +
spec/controllers/home_controller_spec.rb | 20 ++----
spec/lib/permalink_redirector_spec.rb | 4 +-
yarn.lock | 20 ++++++
33 files changed, 423 insertions(+), 145 deletions(-)
delete mode 100644 app/javascript/mastodon/features/ui/components/document_title.js
create mode 100644 app/javascript/mastodon/features/ui/components/sign_in_banner.js
(limited to 'app/serializers')
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 7e443eb9e..29478209d 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,8 +2,8 @@
class HomeController < ApplicationController
before_action :redirect_unauthenticated_to_permalinks!
- before_action :authenticate_user!
before_action :set_referrer_policy_header
+ before_action :set_instance_presenter
def index
@body_classes = 'app-body'
@@ -14,20 +14,16 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks!
return if user_signed_in?
- redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
- end
+ redirect_path = PermalinkRedirector.new(request.path).redirect_path
- def default_redirect_path
- if request.path.start_with?('/web') || whitelist_mode?
- new_user_session_path
- elsif single_user_mode?
- short_account_path(Account.local.without_suspended.where('id > 0').first)
- else
- about_path
- end
+ redirect_to(redirect_path) if redirect_path.present?
end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
+
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index eedf61dc9..f61f06e40 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -536,10 +536,12 @@ export function expandFollowingFail(id, error) {
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
- const loadedRelationships = getState().get('relationships');
+ const state = getState();
+ const loadedRelationships = state.get('relationships');
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+ const signedIn = !!state.getIn(['meta', 'me']);
- if (newAccountIds.length === 0) {
+ if (!signedIn || newAccountIds.length === 0) {
return;
}
diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js
index 16a3df8f6..b7f406cb8 100644
--- a/app/javascript/mastodon/actions/markers.js
+++ b/app/javascript/mastodon/actions/markers.js
@@ -1,6 +1,7 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
+import { List as ImmutableList } from 'immutable';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
@@ -11,7 +12,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
@@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => {
const params = {};
- const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
+ const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
@@ -82,9 +83,10 @@ const _buildParams = (state) => {
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
- const params = _buildParams(getState());
+ const accessToken = getState().getIn(['meta', 'access_token'], '');
+ const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js
index d1c7f08a9..3570b3644 100644
--- a/app/javascript/mastodon/components/logo.js
+++ b/app/javascript/mastodon/components/logo.js
@@ -1,8 +1,8 @@
import React from 'react';
const Logo = () => (
-