Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/serializers/rest/instance_serializer.rb
- app/views/layouts/error.html.haml
- public/oops.png
  Took our version (same as upstream but without the things that only
  make sense in an animation).

Additional changes:
- app/javascript/flavours/vanilla/theme.yml
  Include upstream's javascript in error pages.
This commit is contained in:
Thibaut Girka 2019-02-17 11:58:17 +01:00
commit 369a475a70
18 changed files with 639 additions and 362 deletions

View File

@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
respond_to :json respond_to :json
def show def show
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
end end
end end

View File

@ -7,7 +7,7 @@ pack:
filename: common.js filename: common.js
stylesheet: true stylesheet: true
embed: public.js embed: public.js
error: error: error.js
home: home:
filename: application.js filename: application.js
preload: preload:

View File

@ -78,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent {
] ]
handleReplyClick = () => { handleReplyClick = () => {
if (me) {
this.props.onReply(this.props.status, this.context.router.history); this.props.onReply(this.props.status, this.context.router.history);
} else {
this._openInteractionDialog('reply');
}
} }
handleShareClick = () => { handleShareClick = () => {
@ -91,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent {
} }
handleFavouriteClick = () => { handleFavouriteClick = () => {
if (me) {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} else {
this._openInteractionDialog('favourite');
}
} }
handleReblogClick = (e) => { handleReblogClick = e => {
if (me) {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
} else {
this._openInteractionDialog('reblog');
}
}
_openInteractionDialog = type => {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
} }
handleDeleteClick = () => { handleDeleteClick = () => {
@ -233,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']), accountIds: state.getIn(['user_lists', 'blocks', 'items']),
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, accountIds, shouldUpdateScroll } = this.props; const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='blocks' scrollKey='blocks'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
domains: state.getIn(['domain_lists', 'blocks', 'items']), domains: state.getIn(['domain_lists', 'blocks', 'items']),
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
domains: ImmutablePropTypes.orderedSet, domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, domains, shouldUpdateScroll } = this.props; const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
if (!domains) { if (!domains) {
return ( return (
@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='domain_blocks' scrollKey='domain_blocks'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, shouldUpdateScroll, accountIds } = this.props; const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='follow_requests' scrollKey='follow_requests'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items']), accountIds: state.getIn(['user_lists', 'mutes', 'items']),
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, shouldUpdateScroll, accountIds } = this.props; const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='mutes' scrollKey='mutes'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -87,7 +87,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
} }
render () { render () {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { compact } = this.props; const { compact } = this.props;

View File

@ -0,0 +1,13 @@
import ready from '../mastodon/ready';
ready(() => {
const image = document.querySelector('img');
image.addEventListener('mouseenter', () => {
image.src = '/oops.gif';
});
image.addEventListener('mouseleave', () => {
image.src = '/oops.png';
});
});

View File

@ -100,6 +100,7 @@ body {
vertical-align: middle; vertical-align: middle;
margin: 20px; margin: 20px;
&__illustration {
img { img {
display: block; display: block;
max-width: 470px; max-width: 470px;
@ -107,6 +108,7 @@ body {
height: auto; height: auto;
margin-top: -120px; margin-top: -120px;
} }
}
h1 { h1 {
font-size: 20px; font-size: 20px;

View File

@ -138,11 +138,13 @@ class ActivityPub::Activity
def status_from_object def status_from_object
# If the status is already known, return it # If the status is already known, return it
status = status_from_uri(object_uri) status = status_from_uri(object_uri)
return status unless status.nil? return status unless status.nil?
# If the boosted toot is embedded and it is a self-boost, handle it like a Create # If the boosted toot is embedded and it is a self-boost, handle it like a Create
unless unsupported_object_type? unless unsupported_object_type?
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
if actor_id == @account.uri if actor_id == @account.uri
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
end end
@ -166,4 +168,21 @@ class ActivityPub::Activity
ensure ensure
redis.del(key) redis.del(key)
end end
def fetch?
!@options[:delivery]
end
def followed_by_local_accounts?
@account.passive_relationships.exists?
end
def requested_through_relay?
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
end
def reject_payload!
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
nil
end
end end

View File

@ -2,8 +2,11 @@
class ActivityPub::Activity::Announce < ActivityPub::Activity class ActivityPub::Activity::Announce < ActivityPub::Activity
def perform def perform
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
original_status = status_from_object original_status = status_from_object
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
return reject_payload! if original_status.nil? || !announceable?(original_status)
status = Status.find_by(account: @account, reblog: original_status) status = Status.find_by(account: @account, reblog: original_status)
@ -39,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
def announceable?(status) def announceable?(status)
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
end end
def related_to_local_activity?
followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status?
end
def reblog_of_local_status?
status_from_uri(object_uri)&.account&.local?
end
end end

View File

@ -2,7 +2,7 @@
class ActivityPub::Activity::Create < ActivityPub::Activity class ActivityPub::Activity::Create < ActivityPub::Activity
def perform def perform
return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity? return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
RedisLock.acquire(lock_options) do |lock| RedisLock.acquire(lock_options) do |lock|
if lock.acquired? if lock.acquired?
@ -341,18 +341,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
responds_to_followed_account? || addresses_local_accounts? responds_to_followed_account? || addresses_local_accounts?
end end
def fetch?
!@options[:delivery]
end
def followed_by_local_accounts?
@account.passive_relationships.exists?
end
def requested_through_relay?
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
end
def responds_to_followed_account? def responds_to_followed_account?
!replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?) !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
end end

View File

@ -2,7 +2,7 @@
class REST::ApplicationSerializer < ActiveModel::Serializer class REST::ApplicationSerializer < ActiveModel::Serializer
attributes :id, :name, :website, :redirect_uri, attributes :id, :name, :website, :redirect_uri,
:client_id, :client_secret :client_id, :client_secret, :vapid_key
def id def id
object.id.to_s object.id.to_s
@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer
def website def website
object.website.presence object.website.presence
end end
def vapid_key
Rails.configuration.x.vapid_public_key
end
end end

View File

@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :description, :email, attributes :uri, :title, :description, :email,
:version, :urls, :stats, :thumbnail, :max_toot_chars, :version, :urls, :stats, :thumbnail, :max_toot_chars,
:languages :languages, :registrations
has_one :contact_account, serializer: REST::AccountSerializer has_one :contact_account, serializer: REST::AccountSerializer
@ -55,6 +55,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
[I18n.default_locale] [I18n.default_locale]
end end
def registrations
Setting.open_registrations && !Rails.configuration.x.single_user_mode
end
private private
def instance_presenter def instance_presenter

View File

@ -5,10 +5,12 @@
%meta{ charset: 'utf-8' }/ %meta{ charset: 'utf-8' }/
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ') %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/ %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
= javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
= render partial: 'layouts/theme', object: (@core || { pack: 'common' }) = render partial: 'layouts/theme', object: (@core || { pack: 'common' })
= render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' }) = render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' })
%body.error %body.error
.dialog .dialog
%img{ alt: Setting.default_settings['site_title'], src: current_user&.setting_auto_play_gif ? '/oops.gif' : '/oops.png' }/ .dialog__illustration
%div %img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/
.dialog__message
%h1= yield :content %h1= yield :content

View File

@ -22,7 +22,9 @@ RSpec.describe ActivityPub::Activity::Announce do
end end
describe '#perform' do describe '#perform' do
context 'when sender is followed by a local account' do
before do before do
Fabricate(:account).follow!(sender)
subject.perform subject.perform
end end
@ -67,4 +69,84 @@ RSpec.describe ActivityPub::Activity::Announce do
end end
end end
end end
context 'when the status belongs to a local user' do
before do
subject.perform
end
let(:object_json) do
ActivityPub::TagManager.instance.uri_for(status)
end
it 'creates a reblog by sender of status' do
expect(sender.reblogged?(status)).to be true
end
end
context 'when the sender is relayed' do
let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
context 'and the relay is enabled' do
before do
relay.update(state: :accepted)
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: 'http://example.com/followers',
}
end
it 'creates a reblog by sender of status' do
expect(sender.statuses.count).to eq 2
end
end
context 'and the relay is disabled' do
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: 'http://example.com/followers',
}
end
it 'does not create anything' do
expect(sender.statuses.count).to eq 0
end
end
end
context 'when the sender has no relevance to local activity' do
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: 'http://example.com/followers',
}
end
it 'does not create anything' do
expect(sender.statuses.count).to eq 0
end
end
end
end end

View File

@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do
}.with_indifferent_access }.with_indifferent_access
end end
subject { described_class.new(json, sender) }
before do before do
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
@ -23,6 +21,9 @@ RSpec.describe ActivityPub::Activity::Create do
end end
describe '#perform' do describe '#perform' do
context 'when fetching' do
subject { described_class.new(json, sender) }
before do before do
subject.perform subject.perform
end end
@ -407,4 +408,127 @@ RSpec.describe ActivityPub::Activity::Create do
end end
end end
end end
context 'when sender is followed by local users' do
subject { described_class.new(json, sender, delivery: true) }
before do
Fabricate(:account).follow!(sender)
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
end
context 'when sender replies to local status' do
let!(:local_status) { Fabricate(:status) }
subject { described_class.new(json, sender, delivery: true) }
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
end
context 'when sender targets a local user' do
let!(:local_account) { Fabricate(:account) }
subject { described_class.new(json, sender, delivery: true) }
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(local_account),
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
end
context 'when sender cc\'s a local user' do
let!(:local_account) { Fabricate(:account) }
subject { described_class.new(json, sender, delivery: true) }
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
cc: ActivityPub::TagManager.instance.uri_for(local_account),
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
end
context 'when the sender has no relevance to local activity' do
subject { described_class.new(json, sender, delivery: true) }
before do
subject.perform
end
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
}
end
it 'does not create anything' do
expect(sender.statuses.count).to eq 0
end
end
end
end end