7 Commits 4699cf853c ... bf31f394fb

Author SHA1 Message Date
  dependabot[bot] bf31f394fb Bump json-ld from 2.2.1 to 3.0.2 (#8804) 9 months ago
  dependabot[bot] f424e99e46 Bump brakeman from 4.3.1 to 4.4.0 (#9848) 9 months ago
  ThibG aeb124491d Reject existing Follow in addition to sending a Block (#9811) 9 months ago
  ThibG 75b1488cf4 Add tombstones for remote statuses (#9830) 9 months ago
  Eugen Rochko 31f396b57d Add support for non-public reblogs from ActivityPub (#9841) 9 months ago
  dependabot[bot] 55219f11cc Bump bundler-audit from 0.6.0 to 0.6.1 (#9847) 9 months ago
  Eugen Rochko a492a9bcd3 Add information about how to opt-in to the directory on the directory (#9834) 9 months ago

+ 2 - 2
Gemfile

@@ -89,7 +89,7 @@ gem 'tzinfo-data', '~> 1.2018'
 gem 'webpacker', '~> 3.5'
 gem 'webpush'
 
-gem 'json-ld', '~> 2.2'
+gem 'json-ld', '~> 3.0'
 gem 'json-ld-preloaded', '~> 3.0'
 gem 'rdf-normalize', '~> 0.3'
 
@@ -128,7 +128,7 @@ group :development do
   gem 'letter_opener_web', '~> 1.3'
   gem 'memory_profiler'
   gem 'rubocop', '~> 0.63', require: false
-  gem 'brakeman', '~> 4.3', require: false
+  gem 'brakeman', '~> 4.4', require: false
   gem 'bundler-audit', '~> 0.6', require: false
   gem 'scss_lint', '~> 0.57', require: false
 

+ 7 - 7
Gemfile.lock

@@ -100,14 +100,14 @@ GEM
       debug_inspector (>= 0.0.1)
     bootsnap (1.3.2)
       msgpack (~> 1.0)
-    brakeman (4.3.1)
+    brakeman (4.4.0)
     browser (2.5.3)
     builder (3.2.3)
     bullet (5.9.0)
       activesupport (>= 3.0.0)
       uniform_notifier (~> 1.11)
-    bundler-audit (0.6.0)
-      bundler (~> 1.2)
+    bundler-audit (0.6.1)
+      bundler (>= 1.2.0, < 3)
       thor (~> 0.18)
     byebug (10.0.2)
     capistrano (3.11.0)
@@ -287,7 +287,7 @@ GEM
     jaro_winkler (1.5.2)
     jmespath (1.4.0)
     json (2.1.0)
-    json-ld (2.2.1)
+    json-ld (3.0.2)
       multi_json (~> 1.12)
       rdf (>= 2.2.8, < 4.0)
     json-ld-preloaded (3.0.0)
@@ -471,7 +471,7 @@ GEM
     rb-fsevent (0.10.3)
     rb-inotify (0.9.10)
       ffi (>= 0.5.0, < 2)
-    rdf (3.0.7)
+    rdf (3.0.9)
       hamster (~> 3.0)
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.3.3)
@@ -661,7 +661,7 @@ DEPENDENCIES
   better_errors (~> 2.5)
   binding_of_caller (~> 0.7)
   bootsnap (~> 1.3)
-  brakeman (~> 4.3)
+  brakeman (~> 4.4)
   browser
   bullet (~> 5.9)
   bundler-audit (~> 0.6)
@@ -699,7 +699,7 @@ DEPENDENCIES
   i18n-tasks (~> 0.9)
   idn-ruby
   iso-639
-  json-ld (~> 2.2)
+  json-ld (~> 3.0)
   json-ld-preloaded (~> 3.0)
   kaminari (~> 1.1)
   letter_opener (~> 1.7)

+ 27 - 0
app/javascript/styles/mastodon/widgets.scss

@@ -480,3 +480,30 @@ $fluid-breakpoint: $maximum-width + 20px;
     }
   }
 }
+
+.notice-widget {
+  margin-bottom: 10px;
+  color: $darker-text-color;
+
+  p {
+    margin-bottom: 10px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    font-size: 14px;
+    line-height: 20px;
+    text-decoration: none;
+    font-weight: 500;
+    color: $ui-highlight-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+}

+ 13 - 1
app/lib/activitypub/activity/announce.rb

@@ -17,7 +17,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
       uri: @json['id'],
       created_at: @json['published'],
       override_timestamps: @options[:override_timestamps],
-      visibility: original_status.visibility
+      visibility: visibility_from_audience
     )
 
     distribute(status)
@@ -26,6 +26,18 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
 
   private
 
+  def visibility_from_audience
+    if equals_or_includes?(@json['to'], ActivityPub::TagManager::COLLECTIONS[:public])
+      :public
+    elsif equals_or_includes?(@json['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
+      :unlisted
+    elsif equals_or_includes?(@json['to'], @account.followers_url)
+      :private
+    else
+      :direct
+    end
+  end
+
   def announceable?(status)
     status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
   end

+ 1 - 0
app/lib/activitypub/activity/create.rb

@@ -6,6 +6,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
   def perform
     return if unsupported_object_type? || invalid_origin?(@object['id'])
+    return if Tombstone.exists?(uri: @object['id'])
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?

+ 12 - 2
app/lib/activitypub/activity/delete.rb

@@ -21,8 +21,9 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
   def delete_note
     return if object_uri.nil?
 
-    RedisLock.acquire(lock_options) do |_lock|
-      delete_later!(object_uri)
+    unless invalid_origin?(object_uri)
+      RedisLock.acquire(lock_options) { |_lock| delete_later!(object_uri) }
+      Tombstone.find_or_create_by(uri: object_uri, account: @account)
     end
 
     @status   = Status.find_by(uri: object_uri, account: @account)
@@ -74,4 +75,13 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
   def lock_options
     { redis: Redis.current, key: "create:#{object_uri}" }
   end
+
+  def invalid_origin?(url)
+    return true if unsupported_uri_scheme?(url)
+
+    needle   = Addressable::URI.parse(url).host
+    haystack = Addressable::URI.parse(@account.uri).host
+
+    !haystack.casecmp(needle).zero?
+  end
 end

+ 2 - 2
app/models/status.rb

@@ -478,7 +478,7 @@ class Status < ApplicationRecord
     return if direct_visibility?
 
     account&.increment_count!(:statuses_count)
-    reblog&.increment_count!(:reblogs_count) if reblog?
+    reblog&.increment_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
     thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 
@@ -486,7 +486,7 @@ class Status < ApplicationRecord
     return if direct_visibility? || marked_for_mass_destruction?
 
     account&.decrement_count!(:statuses_count)
-    reblog&.decrement_count!(:reblogs_count) if reblog?
+    reblog&.decrement_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?)
     thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 

+ 15 - 0
app/models/tombstone.rb

@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: tombstones
+#
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)
+#  uri        :string           not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Tombstone < ApplicationRecord
+end

+ 6 - 0
app/services/activitypub/process_account_service.rb

@@ -33,6 +33,8 @@ class ActivityPub::ProcessAccountService < BaseService
 
     after_protocol_change! if protocol_changed?
     after_key_change! if key_changed? && !@options[:signed_with_known_key]
+    clear_tombstones! if key_changed?
+
     unless @options[:only_key]
       check_featured_collection! if @account.featured_collection_url.present?
       check_links! unless @account.fields.empty?
@@ -209,6 +211,10 @@ class ActivityPub::ProcessAccountService < BaseService
     !@old_public_key.nil? && @old_public_key != @account.public_key
   end
 
+  def clear_tombstones!
+    Tombstone.delete_all(account_id: @account.id)
+  end
+
   def protocol_changed?
     !@old_protocol.nil? && @old_protocol != @account.protocol
   end

+ 15 - 0
app/services/unfollow_service.rb

@@ -20,6 +20,7 @@ class UnfollowService < BaseService
 
     follow.destroy!
     create_notification(follow) unless @target_account.local?
+    create_reject_notification(follow) if @target_account.local? && !@source_account.local?
     UnmergeWorker.perform_async(@target_account.id, @source_account.id)
     follow
   end
@@ -42,6 +43,12 @@ class UnfollowService < BaseService
     end
   end
 
+  def create_reject_notification(follow)
+    # Rejecting an already-existing follow request
+    return unless follow.account.activitypub?
+    ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url)
+  end
+
   def build_json(follow)
     ActiveModelSerializers::SerializableResource.new(
       follow,
@@ -50,6 +57,14 @@ class UnfollowService < BaseService
     ).to_json
   end
 
+  def build_reject_json(follow)
+    ActiveModelSerializers::SerializableResource.new(
+      follow,
+      serializer: ActivityPub::RejectFollowSerializer,
+      adapter: ActivityPub::Adapter
+    ).to_json
+  end
+
   def build_xml(follow)
     OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow))
   end

+ 16 - 2
app/views/directories/index.html.haml

@@ -41,8 +41,22 @@
       = paginate @accounts
 
   .column-1
-    - if @tags.empty?
-      .nothing-here.nothing-here--flexible
+    - if user_signed_in?
+      .box-widget.notice-widget
+        - if current_account.discoverable?
+          - if current_account.followers_count < Account::MIN_FOLLOWERS_DISCOVERY
+            %p= t('directories.enabled_but_waiting', min_followers: Account::MIN_FOLLOWERS_DISCOVERY)
+          - else
+            %p= t('directories.enabled')
+        - else
+          %p= t('directories.how_to_enable')
+
+          = link_to settings_profile_path do
+            = t('settings.edit_profile')
+            = fa_icon 'chevron-right fw'
+
+    - if @tags.empty? && !user_signed_in?
+      .nothing-here
     - else
       - @tags.each do |tag|
         .directory__tag{ class: tag.id == @tag&.id ? 'active' : nil }

+ 3 - 0
config/locales/en.yml

@@ -553,8 +553,11 @@ en:
     warning_title: Disseminated content availability
   directories:
     directory: Profile directory
+    enabled: You are currently listed in the directory.
+    enabled_but_waiting: You have opted-in to be listed in the directory, but you do not have the minimum number of followers (%{min_followers}) to be listed yet.
     explanation: Discover users based on their interests
     explore_mastodon: Explore %{title}
+    how_to_enable: You are not currently opted-in to the directory. You can opt-in below. Use hashtags in your bio text to be listed under specific hashtags!
     people:
       one: "%{count} person"
       other: "%{count} people"

+ 12 - 0
db/migrate/20190117114553_create_tombstones.rb

@@ -0,0 +1,12 @@
+class CreateTombstones < ActiveRecord::Migration[5.2]
+  def change
+    create_table :tombstones do |t|
+      t.belongs_to :account, foreign_key: { on_delete: :cascade }
+      t.string :uri, null: false
+
+      t.timestamps
+    end
+
+    add_index :tombstones, :uri
+  end
+end

+ 11 - 1
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: 2019_01_03_124754) do
+ActiveRecord::Schema.define(version: 2019_01_17_114553) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -615,6 +615,15 @@ ActiveRecord::Schema.define(version: 2019_01_03_124754) do
     t.index ["name"], name: "index_tags_on_name", unique: true
   end
 
+  create_table "tombstones", force: :cascade do |t|
+    t.bigint "account_id"
+    t.string "uri", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id"], name: "index_tombstones_on_account_id"
+    t.index ["uri"], name: "index_tombstones_on_uri"
+  end
+
   create_table "users", force: :cascade do |t|
     t.string "email", default: "", null: false
     t.datetime "created_at", null: false
@@ -743,6 +752,7 @@ ActiveRecord::Schema.define(version: 2019_01_03_124754) do
   add_foreign_key "statuses_tags", "tags", name: "fk_3081861e21", on_delete: :cascade
   add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade
   add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade
+  add_foreign_key "tombstones", "accounts", on_delete: :cascade
   add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
   add_foreign_key "users", "invites", on_delete: :nullify
   add_foreign_key "users", "oauth_applications", column: "created_by_application_id", on_delete: :nullify

+ 18 - 0
spec/services/unfollow_service_spec.rb

@@ -56,4 +56,22 @@ RSpec.describe UnfollowService, type: :service do
       expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub (reverse)' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      bob.follow!(sender)
+      stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+      subject.call(bob, sender)
+    end
+
+    it 'destroys the following relation' do
+      expect(bob.following?(sender)).to be false
+    end
+
+    it 'sends a reject activity' do
+      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
+    end
+  end
 end