]> cat aescling's git repositories - mastodon.git/commitdiff
Private visibility on statuses prevents non-followers from seeing those
authorEugen Rochko <eugen@zeonfederated.com>
Wed, 21 Dec 2016 19:00:18 +0000 (20:00 +0100)
committerEugen Rochko <eugen@zeonfederated.com>
Wed, 21 Dec 2016 19:04:13 +0000 (20:04 +0100)
Filters out hidden stream entries from Atom feed
Blocks now generate hidden stream entries, can be used to federate blocks
Private statuses cannot be reblogged (generates generic 422 error for now)
POST /api/v1/statuses now takes visibility=(public|unlisted|private) param instead of unlisted boolean
Statuses JSON now contains visibility=(public|unlisted|private) field

17 files changed:
app/assets/javascripts/components/actions/compose.jsx
app/controllers/accounts_controller.rb
app/controllers/api/v1/accounts_controller.rb
app/controllers/api/v1/statuses_controller.rb
app/controllers/stream_entries_controller.rb
app/models/block.rb
app/models/concerns/streamable.rb
app/models/status.rb
app/models/stream_entry.rb
app/services/post_status_service.rb
app/services/reblog_service.rb
app/views/api/v1/statuses/_show.rabl
db/migrate/20161221152630_add_hidden_to_stream_entries.rb [new file with mode: 0644]
db/schema.rb
public/404.html
public/422.html [deleted file]
spec/controllers/api/v1/statuses_controller_spec.rb

index a9fbe6b9180ce321cb31c4abf2ca18e464bff00a..fdb0abdcd554198ac86b5df6e0dbf919684ec8c6 100644 (file)
@@ -67,7 +67,7 @@ export function submitCompose() {
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
       media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
       sensitive: getState().getIn(['compose', 'sensitive']),
-      unlisted: getState().getIn(['compose', 'unlisted'])
+      visibility: getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public'
     }).then(function (response) {
       dispatch(submitComposeSuccess({ ...response.data }));
 
index 57f25a2736f5a871ef2ddb9f990ae23110e12a5c..411a41ccc2965909c5e19eb0c82d1a57bf5c9154 100644 (file)
@@ -11,12 +11,12 @@ class AccountsController < ApplicationController
   def show
     respond_to do |format|
       format.html do
-        @statuses = @account.statuses.order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id])
+        @statuses = @account.statuses.permitted_for(@account, current_account).order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id])
         @statuses = cache_collection(@statuses, Status)
       end
 
       format.atom do
-        @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+        @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
       end
     end
   end
index 0abdfd9fadc3ac2944a08f9caf03e5c3a7426441..de53a9602657285d3e29412a50a0538047423a19 100644 (file)
@@ -8,8 +8,7 @@ class Api::V1::AccountsController < ApiController
 
   respond_to :json
 
-  def show
-  end
+  def show; end
 
   def verify_credentials
     @account = current_user.account
@@ -47,7 +46,7 @@ class Api::V1::AccountsController < ApiController
   end
 
   def statuses
-    @statuses = @account.statuses.paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
+    @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
     @statuses = cache_collection(@statuses, Status)
 
     set_maps(@statuses)
index 453d003da232b65f4d04b50e03b2d694f7159504..f7b4ed6100a29124ea1c6e9cb86a9c003bcc6535 100644 (file)
@@ -52,7 +52,7 @@ class Api::V1::StatusesController < ApiController
   end
 
   def create
-    @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive], unlisted: params[:unlisted])
+    @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive], visibility: params[:visibility])
     render action: :show
   end
 
@@ -95,5 +95,6 @@ class Api::V1::StatusesController < ApiController
 
   def set_status
     @status = Status.find(params[:id])
+    raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
   end
 end
index 58dd423f776c2e82495747fbce5f2ef08f1f893c..438d51a84cb7def4cec2612f6095899c0d593b02 100644 (file)
@@ -14,8 +14,8 @@ class StreamEntriesController < ApplicationController
         return gone if @stream_entry.activity.nil?
 
         if @stream_entry.activity_type == 'Status'
-          @ancestors   = @stream_entry.activity.ancestors
-          @descendants = @stream_entry.activity.descendants
+          @ancestors   = @stream_entry.activity.ancestors(current_account)
+          @descendants = @stream_entry.activity.descendants(current_account)
         end
       end
 
@@ -43,7 +43,7 @@ class StreamEntriesController < ApplicationController
   end
 
   def set_stream_entry
-    @stream_entry = @account.stream_entries.find(params[:id])
+    @stream_entry = @account.stream_entries.where(hidden: false).find(params[:id])
     @type         = @stream_entry.activity_type.downcase
   end
 
index dc05bce877e28d07d307a0321856a1e4ca490b40..ad225d18083edb6ad454b74a42a5e24b386ef560 100644 (file)
@@ -1,9 +1,31 @@
 # frozen_string_literal: true
 
 class Block < ApplicationRecord
+  include Streamable
+
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
   validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
+
+  def verb
+    destroyed? ? :unblock : :block
+  end
+
+  def target
+    target_account
+  end
+
+  def object_type
+    :person
+  end
+
+  def hidden?
+    true
+  end
+
+  def title
+    destroyed? ? "#{account.acct} is no longer blocking #{target_account.acct}" : "#{account.acct} blocked #{target_account.acct}"
+  end
 end
index d9f5dc4d863834b7a8d27218138fe6782b9a54bc..58c15cfbcea02d5a26a1ea3cf1743d796eabb4a8 100644 (file)
@@ -26,8 +26,12 @@ module Streamable
       super
     end
 
+    def hidden?
+      false
+    end
+
     after_create do
-      account.stream_entries.create!(activity: self) if account.local?
+      account.stream_entries.create!(activity: self, hidden: hidden?) if account.local?
     end
   end
 end
index e87828e328b83b12d2ada71505dd667de67595fe..603f3b7a2222e06c2a9a662ff36ba924d9da94f7 100644 (file)
@@ -5,7 +5,7 @@ class Status < ApplicationRecord
   include Streamable
   include Cacheable
 
-  enum visibility: [:public, :unlisted], _suffix: :visibility
+  enum visibility: [:public, :unlisted, :private], _suffix: :visibility
 
   belongs_to :account, inverse_of: :statuses
 
@@ -66,19 +66,19 @@ class Status < ApplicationRecord
     content
   end
 
-  def reblogs_count
-    attributes['reblogs_count'] || reblogs.count
+  def hidden?
+    private_visibility?
   end
 
-  def favourites_count
-    attributes['favourites_count'] || favourites.count
+  def permitted?(other_account = nil)
+    private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : true
   end
 
   def ancestors(account = nil)
     ids      = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', id]) - [self]).pluck(:id)
     statuses = Status.where(id: ids).with_includes.group_by(&:id)
     results  = ids.map { |id| statuses[id].first }
-    results  = results.reject { |status| account.blocking?(status.account) } unless account.nil?
+    results  = results.reject { |status| filter_from_context?(status, account) }
 
     results
   end
@@ -87,7 +87,7 @@ class Status < ApplicationRecord
     ids      = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', id]) - [self]).pluck(:id)
     statuses = Status.where(id: ids).with_includes.group_by(&:id)
     results  = ids.map { |id| statuses[id].first }
-    results  = results.reject { |status| account.blocking?(status.account) } unless account.nil?
+    results  = results.reject { |status| filter_from_context?(status, account) }
 
     results
   end
@@ -128,6 +128,14 @@ class Status < ApplicationRecord
       select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
     end
 
+    def permitted_for(target_account, account)
+      if account&.id == target_account.id || account&.following?(target_account)
+        self
+      else
+        where.not(visibility: :private)
+      end
+    end
+
     def reload_stale_associations!(cached_items)
       account_ids = []
 
@@ -161,5 +169,12 @@ class Status < ApplicationRecord
   before_validation do
     text.strip!
     self.in_reply_to_account_id = thread.account_id if reply?
+    self.visibility             = :public if visibility.nil?
+  end
+
+  private
+
+  def filter_from_context?(status, account)
+    account&.blocking?(status.account) || !status.permitted?(account)
   end
 end
index f6c8f461b306339667f71717ac277643b8643d0d..fcc691befdda9922221f32f1102ff5700782b14a 100644 (file)
@@ -9,6 +9,7 @@ class StreamEntry < ApplicationRecord
   belongs_to :status,    foreign_type: 'Status',    foreign_key: 'activity_id'
   belongs_to :follow,    foreign_type: 'Follow',    foreign_key: 'activity_id'
   belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id'
+  belongs_to :block,     foreign_type: 'Block',     foreign_key: 'activity_id'
 
   validates :account, :activity, presence: true
 
@@ -29,7 +30,7 @@ class StreamEntry < ApplicationRecord
   end
 
   def targeted?
-    [:follow, :share, :favorite].include? verb
+    [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
   end
 
   def target
@@ -57,7 +58,7 @@ class StreamEntry < ApplicationRecord
   end
 
   def activity
-    send(activity_type.downcase.to_sym)
+    !new_record? ? send(activity_type.downcase) : super
   end
 
   private
index d5204151b00cc25f381481a8e66cb9dd1302e5af..55405c0dbed7d8184f611bf1a987504d30a69e0f 100644 (file)
@@ -10,7 +10,7 @@ class PostStatusService < BaseService
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @return [Status]
   def call(account, text, in_reply_to = nil, options = {})
-    status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:unlisted] ? :unlisted : :public)
+    status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:visibility])
     attach_media(status, options[:media_ids])
     process_mentions_service.call(status)
     process_hashtags_service.call(status)
index 7d0c90d2f65cf455b970855b3fed6389b37278f9..1a78b8f697fb9958368a59b0afb6aea4756b6d2b 100644 (file)
@@ -6,6 +6,8 @@ class ReblogService < BaseService
   # @param [Status] reblogged_status Status to be reblogged
   # @return [Status]
   def call(account, reblogged_status)
+    raise ActiveRecord::RecordInvalid if reblogged_status.private_visibility?
+
     reblog = account.statuses.create!(reblog: reblogged_status, text: '')
 
     DistributionWorker.perform_async(reblog.id)
index 579c47b26d8635e86f9aa8696c605e3744bba294..a3391a67e77367649b13ad9e577f2db134462de0 100644 (file)
@@ -1,10 +1,10 @@
-attributes :id, :created_at, :in_reply_to_id, :sensitive
+attributes :id, :created_at, :in_reply_to_id, :sensitive, :visibility
 
 node(:uri)              { |status| TagManager.instance.uri_for(status) }
 node(:content)          { |status| Formatter.instance.format(status) }
 node(:url)              { |status| TagManager.instance.url_for(status) }
-node(:reblogs_count)    { |status| defined?(@reblogs_counts_map)    ? (@reblogs_counts_map[status.id]    || 0) : status.reblogs_count }
-node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }
+node(:reblogs_count)    { |status| defined?(@reblogs_counts_map)    ? (@reblogs_counts_map[status.id]    || 0) : status.reblogs.count }
+node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites.count }
 
 child :account do
   extends 'api/v1/accounts/show'
diff --git a/db/migrate/20161221152630_add_hidden_to_stream_entries.rb b/db/migrate/20161221152630_add_hidden_to_stream_entries.rb
new file mode 100644 (file)
index 0000000..0d2def7
--- /dev/null
@@ -0,0 +1,5 @@
+class AddHiddenToStreamEntries < ActiveRecord::Migration[5.0]
+  def change
+    add_column :stream_entries, :hidden, :boolean, null: false, default: false
+  end
+end
index 4f23cf144020a03a3b0045910a40c14bab4ad3ba..706099897202e6b0f5b5b2a21ca2ff02d59fcdb7 100644 (file)
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20161205214545) do
+ActiveRecord::Schema.define(version: 20161221152630) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -196,8 +196,9 @@ ActiveRecord::Schema.define(version: 20161205214545) do
     t.integer  "account_id"
     t.integer  "activity_id"
     t.string   "activity_type"
-    t.datetime "created_at",    null: false
-    t.datetime "updated_at",    null: false
+    t.datetime "created_at",                    null: false
+    t.datetime "updated_at",                    null: false
+    t.boolean  "hidden",        default: false, null: false
     t.index ["account_id"], name: "index_stream_entries_on_account_id", using: :btree
     t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree
   end
index 514a935e23d45a65e028a18f4f58b6fc2579a2c5..eecfd6743820883829567f7cf687252644cc525b 100644 (file)
@@ -2,67 +2,42 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
-  <title>The page you were looking for doesn't exist (404)</title>
+  <title>The page you were looking for doesn't exist</title>
   <meta name="viewport" content="width=device-width,initial-scale=1">
+  <link href="https://fonts.googleapis.com/css?family=Roboto:400" rel="stylesheet">
   <style>
-  body {
-    background-color: #EFEFEF;
-    color: #2E2F30;
-    text-align: center;
-    font-family: arial, sans-serif;
-    margin: 0;
-  }
+    body {
+      font-family: 'Roboto', sans-serif;
+      background: #282c37;
+      color: #9baec8;
+      text-align: center;
+      margin: 0;
+      padding: 20px;
+    }
 
-  div.dialog {
-    width: 95%;
-    max-width: 33em;
-    margin: 4em auto 0;
-  }
+    .dialog img {
+      display: block;
+      margin: 20px auto;
+      margin-top: 50px;
+      max-width: 600px;
+      width: 100%;
+      height: auto;
+    }
 
-  div.dialog > div {
-    border: 1px solid #CCC;
-    border-right-color: #999;
-    border-left-color: #999;
-    border-bottom-color: #BBB;
-    border-top: #B00100 solid 4px;
-    border-top-left-radius: 9px;
-    border-top-right-radius: 9px;
-    background-color: white;
-    padding: 7px 12% 0;
-    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
-  }
-
-  h1 {
-    font-size: 100%;
-    color: #730E15;
-    line-height: 1.5em;
-  }
-
-  div.dialog > p {
-    margin: 0 0 1em;
-    padding: 1em;
-    background-color: #F7F7F7;
-    border: 1px solid #CCC;
-    border-right-color: #999;
-    border-left-color: #999;
-    border-bottom-color: #999;
-    border-bottom-left-radius: 4px;
-    border-bottom-right-radius: 4px;
-    border-top-color: #DADADA;
-    color: #666;
-    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
-  }
+    .dialog h1 {
+      font: 20px/28px 'Roboto', sans-serif;
+      font-weight: 400;
+    }
   </style>
 </head>
 
 <body>
-  <!-- This file lives in public/404.html -->
   <div class="dialog">
+    <img src="/oops.png" alt="Mastodon" />
+
     <div>
-      <h1>The page you were looking for doesn't exist.</h1>
-      <p>You may have mistyped the address or the page may have moved.</p>
+      <h1>The page you were looking for doesn't exist</h1>
     </div>
-    <p>If you are the application owner check the logs for more information.</p>
   </div>
 </body>
 </html>
diff --git a/public/422.html b/public/422.html
deleted file mode 100644 (file)
index eb3601e..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="utf-8">
-  <title>The change you wanted was rejected (422)</title>
-  <meta name="viewport" content="width=device-width,initial-scale=1">
-  <style>
-  body {
-    background-color: #EFEFEF;
-    color: #2E2F30;
-    text-align: center;
-    font-family: arial, sans-serif;
-    margin: 0;
-  }
-
-  div.dialog {
-    width: 95%;
-    max-width: 33em;
-    margin: 4em auto 0;
-  }
-
-  div.dialog > div {
-    border: 1px solid #CCC;
-    border-right-color: #999;
-    border-left-color: #999;
-    border-bottom-color: #BBB;
-    border-top: #B00100 solid 4px;
-    border-top-left-radius: 9px;
-    border-top-right-radius: 9px;
-    background-color: white;
-    padding: 7px 12% 0;
-    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
-  }
-
-  h1 {
-    font-size: 100%;
-    color: #730E15;
-    line-height: 1.5em;
-  }
-
-  div.dialog > p {
-    margin: 0 0 1em;
-    padding: 1em;
-    background-color: #F7F7F7;
-    border: 1px solid #CCC;
-    border-right-color: #999;
-    border-left-color: #999;
-    border-bottom-color: #999;
-    border-bottom-left-radius: 4px;
-    border-bottom-right-radius: 4px;
-    border-top-color: #DADADA;
-    color: #666;
-    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
-  }
-  </style>
-</head>
-
-<body>
-  <!-- This file lives in public/422.html -->
-  <div class="dialog">
-    <div>
-      <h1>The change you wanted was rejected.</h1>
-      <p>Maybe you tried to change something you didn't have access to.</p>
-    </div>
-    <p>If you are the application owner check the logs for more information.</p>
-  </div>
-</body>
-</html>
index 9b027daf8bc67cc225d35cf9c59278eef40da380..ab918fe508f290cc5f6137e144b451d58a0dfc6e 100644 (file)
@@ -97,7 +97,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     it 'updates the reblogs count' do
-      expect(status.reblogs_count).to eq 1
+      expect(status.reblogs.count).to eq 1
     end
 
     it 'updates the reblogged attribute' do
@@ -126,7 +126,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     it 'updates the reblogs count' do
-      expect(status.reblogs_count).to eq 0
+      expect(status.reblogs.count).to eq 0
     end
 
     it 'updates the reblogged attribute' do
@@ -146,7 +146,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     it 'updates the favourites count' do
-      expect(status.favourites_count).to eq 1
+      expect(status.favourites.count).to eq 1
     end
 
     it 'updates the favourited attribute' do
@@ -175,7 +175,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     it 'updates the favourites count' do
-      expect(status.favourites_count).to eq 0
+      expect(status.favourites.count).to eq 0
     end
 
     it 'updates the favourited attribute' do