Merge pull request #253 from glitch-soc/prevent-local-only-federation
prevent federation of local-only statuses
This commit is contained in:
commit
e202efdf8a
|
@ -49,7 +49,7 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_statuses
|
def default_statuses
|
||||||
@account.statuses.where(visibility: [:public, :unlisted])
|
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
|
||||||
end
|
end
|
||||||
|
|
||||||
def only_media_scope
|
def only_media_scope
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
# account_id :integer not null
|
# account_id :integer not null
|
||||||
# application_id :integer
|
# application_id :integer
|
||||||
# in_reply_to_account_id :integer
|
# in_reply_to_account_id :integer
|
||||||
|
# local_only :boolean
|
||||||
#
|
#
|
||||||
|
|
||||||
class Status < ApplicationRecord
|
class Status < ApplicationRecord
|
||||||
|
@ -74,6 +75,8 @@ class Status < ApplicationRecord
|
||||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||||
|
|
||||||
|
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
||||||
|
|
||||||
cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
|
cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
@ -138,6 +141,8 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
around_create Mastodon::Snowflake::Callbacks
|
around_create Mastodon::Snowflake::Callbacks
|
||||||
|
|
||||||
|
before_create :set_locality
|
||||||
|
|
||||||
before_validation :prepare_contents, if: :local?
|
before_validation :prepare_contents, if: :local?
|
||||||
before_validation :set_reblog
|
before_validation :set_reblog
|
||||||
before_validation :set_visibility
|
before_validation :set_visibility
|
||||||
|
@ -218,7 +223,7 @@ class Status < ApplicationRecord
|
||||||
visibility = [:public, :unlisted]
|
visibility = [:public, :unlisted]
|
||||||
|
|
||||||
if account.nil?
|
if account.nil?
|
||||||
where(visibility: visibility)
|
where(visibility: visibility).not_local_only
|
||||||
elsif target_account.blocking?(account) # get rid of blocked peeps
|
elsif target_account.blocking?(account) # get rid of blocked peeps
|
||||||
none
|
none
|
||||||
elsif account.id == target_account.id # author can see own stuff
|
elsif account.id == target_account.id # author can see own stuff
|
||||||
|
@ -257,7 +262,7 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_timeline_default(query)
|
def filter_timeline_default(query)
|
||||||
query.excluding_silenced_accounts
|
query.not_local_only.excluding_silenced_accounts
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_silencing_filter(account)
|
def account_silencing_filter(account)
|
||||||
|
@ -269,9 +274,13 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_only?
|
def marked_local_only?
|
||||||
# match both with and without U+FE0F (the emoji variation selector)
|
# match both with and without U+FE0F (the emoji variation selector)
|
||||||
/👁\ufe0f?\z/.match?(content)
|
/#{local_only_emoji}\ufe0f?\z/.match?(content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_only_emoji
|
||||||
|
'👁'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -299,6 +308,12 @@ class Status < ApplicationRecord
|
||||||
self.sensitive = sensitive || spoiler_text.present?
|
self.sensitive = sensitive || spoiler_text.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_locality
|
||||||
|
if account.domain.nil? && !attribute_changed?(:local_only)
|
||||||
|
self.local_only = marked_local_only?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
|
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,7 @@ class PostStatusService < BaseService
|
||||||
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
|
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
|
||||||
DistributionWorker.perform_async(status.id)
|
DistributionWorker.perform_async(status.id)
|
||||||
|
|
||||||
# match both with and without U+FE0F (the emoji variation selector)
|
unless status.local_only?
|
||||||
unless /👁\ufe0f?\z/.match?(status.content)
|
|
||||||
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
|
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
|
||||||
ActivityPub::DistributionWorker.perform_async(status.id)
|
ActivityPub::DistributionWorker.perform_async(status.id)
|
||||||
ActivityPub::ReplyDistributionWorker.perform_async(status.id) if status.reply? && status.thread.account.local?
|
ActivityPub::ReplyDistributionWorker.perform_async(status.id) if status.reply? && status.thread.account.local?
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ReblogService < BaseService
|
||||||
|
|
||||||
DistributionWorker.perform_async(reblog.id)
|
DistributionWorker.perform_async(reblog.id)
|
||||||
|
|
||||||
unless /👁$/.match?(reblogged_status.content)
|
unless reblogged_status.local_only?
|
||||||
Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
|
Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
|
||||||
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddLocalOnlyFlagToStatuses < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :local_only, :boolean
|
||||||
|
end
|
||||||
|
end
|
|
@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20171212195226) do
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "application_id"
|
t.bigint "application_id"
|
||||||
t.bigint "in_reply_to_account_id"
|
t.bigint "in_reply_to_account_id"
|
||||||
|
t.boolean "local_only"
|
||||||
t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
|
t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
|
||||||
t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
|
t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
|
||||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
|
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace :glitchsoc do
|
||||||
|
desc 'Backfill local-only flag on statuses table'
|
||||||
|
task backfill_local_only: :environment do
|
||||||
|
Status.local.where(local_only: nil).find_each do |st|
|
||||||
|
ActiveRecord::Base.logger.silence { st.update_attribute(:local_only, st.marked_local_only?) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -197,6 +197,43 @@ RSpec.describe Status, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'on create' do
|
||||||
|
let(:local_account) { Fabricate(:account, username: 'local', domain: nil) }
|
||||||
|
let(:remote_account) { Fabricate(:account, username: 'remote', domain: 'example.com') }
|
||||||
|
|
||||||
|
subject { Status.new }
|
||||||
|
|
||||||
|
describe 'on a status that ends with the local-only emoji' do
|
||||||
|
before do
|
||||||
|
subject.text = 'A toot ' + subject.local_only_emoji
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if the status originates from this instance' do
|
||||||
|
before do
|
||||||
|
subject.account = local_account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is marked local-only' do
|
||||||
|
subject.save!
|
||||||
|
|
||||||
|
expect(subject).to be_local_only
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if the status is remote' do
|
||||||
|
before do
|
||||||
|
subject.account = remote_account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not marked local-only' do
|
||||||
|
subject.save!
|
||||||
|
|
||||||
|
expect(subject).to_not be_local_only
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.mutes_map' do
|
describe '.mutes_map' do
|
||||||
let(:status) { Fabricate(:status) }
|
let(:status) { Fabricate(:status) }
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
|
@ -549,6 +586,32 @@ RSpec.describe Status, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with local-only statuses' do
|
||||||
|
let(:status) { Fabricate(:status, local_only: true) }
|
||||||
|
|
||||||
|
subject { Status.as_public_timeline(viewer) }
|
||||||
|
|
||||||
|
context 'without a viewer' do
|
||||||
|
let(:viewer) { nil }
|
||||||
|
|
||||||
|
it 'excludes local-only statuses' do
|
||||||
|
expect(subject).to_not include(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a viewer' do
|
||||||
|
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||||
|
|
||||||
|
it 'includes local-only statuses' do
|
||||||
|
expect(subject).to include(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: What happens if the viewer is remote?
|
||||||
|
# Can the viewer be remote?
|
||||||
|
# What prevents the viewer from being remote?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.as_tag_timeline' do
|
describe '.as_tag_timeline' do
|
||||||
|
@ -570,6 +633,27 @@ RSpec.describe Status, type: :model do
|
||||||
results = Status.as_tag_timeline(tag)
|
results = Status.as_tag_timeline(tag)
|
||||||
expect(results).to include(status)
|
expect(results).to include(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'on a local-only status' do
|
||||||
|
let(:tag) { Fabricate(:tag) }
|
||||||
|
let(:status) { Fabricate(:status, local_only: true, tags: [tag]) }
|
||||||
|
|
||||||
|
context 'without a viewer' do
|
||||||
|
let(:viewer) { nil }
|
||||||
|
|
||||||
|
it 'filters the local-only status out of the result set' do
|
||||||
|
expect(Status.as_tag_timeline(tag, viewer)).not_to include(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a viewer' do
|
||||||
|
let(:viewer) { Fabricate(:account, username: 'viewer', domain: nil) }
|
||||||
|
|
||||||
|
it 'keeps the local-only status in the result set' do
|
||||||
|
expect(Status.as_tag_timeline(tag, viewer)).to include(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.permitted_for' do
|
describe '.permitted_for' do
|
||||||
|
|
Reference in New Issue