Merge pull request #253 from glitch-soc/prevent-local-only-federation
prevent federation of local-only statuses
This commit is contained in:
		
						commit
						978e5e5606
					
				
					 8 changed files with 120 additions and 8 deletions
				
			
		|  | @ -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" | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								lib/tasks/glitchsoc.rake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								lib/tasks/glitchsoc.rake
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue