From d417da7d3ab1a8cb4a8f6b8a7249adc62acb8539 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 21 Dec 2016 20:00:18 +0100 Subject: [PATCH] Private visibility on statuses prevents non-followers from seeing those 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 --- .../components/actions/compose.jsx | 2 +- app/controllers/accounts_controller.rb | 4 +- app/controllers/api/v1/accounts_controller.rb | 5 +- app/controllers/api/v1/statuses_controller.rb | 3 +- app/controllers/stream_entries_controller.rb | 6 +- app/models/block.rb | 22 ++++++ app/models/concerns/streamable.rb | 6 +- app/models/status.rb | 29 +++++-- app/models/stream_entry.rb | 5 +- app/services/post_status_service.rb | 2 +- app/services/reblog_service.rb | 2 + app/views/api/v1/statuses/_show.rabl | 6 +- ...1221152630_add_hidden_to_stream_entries.rb | 5 ++ db/schema.rb | 7 +- public/404.html | 75 +++++++------------ public/422.html | 68 ----------------- .../api/v1/statuses_controller_spec.rb | 8 +- 17 files changed, 106 insertions(+), 149 deletions(-) create mode 100644 db/migrate/20161221152630_add_hidden_to_stream_entries.rb delete mode 100644 public/422.html diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index a9fbe6b918..fdb0abdcd5 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -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 })); diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 57f25a2736..411a41ccc2 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0abdfd9fad..de53a96026 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -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) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 453d003da2..f7b4ed6100 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -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 diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 58dd423f77..438d51a84c 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -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 diff --git a/app/models/block.rb b/app/models/block.rb index dc05bce877..ad225d1808 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -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 diff --git a/app/models/concerns/streamable.rb b/app/models/concerns/streamable.rb index d9f5dc4d86..58c15cfbce 100644 --- a/app/models/concerns/streamable.rb +++ b/app/models/concerns/streamable.rb @@ -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 diff --git a/app/models/status.rb b/app/models/status.rb index e87828e328..603f3b7a22 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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 diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index f6c8f461b3..fcc691befd 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -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 diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index d5204151b0..55405c0dbe 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -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) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 7d0c90d2f6..1a78b8f697 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -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) diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl index 579c47b26d..a3391a67e7 100644 --- a/app/views/api/v1/statuses/_show.rabl +++ b/app/views/api/v1/statuses/_show.rabl @@ -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 index 0000000000..0d2def7f87 --- /dev/null +++ b/db/migrate/20161221152630_add_hidden_to_stream_entries.rb @@ -0,0 +1,5 @@ +class AddHiddenToStreamEntries < ActiveRecord::Migration[5.0] + def change + add_column :stream_entries, :hidden, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 4f23cf1440..7060998972 100644 --- a/db/schema.rb +++ b/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: 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 diff --git a/public/404.html b/public/404.html index 514a935e23..eecfd67438 100644 --- a/public/404.html +++ b/public/404.html @@ -2,67 +2,42 @@ - The page you were looking for doesn't exist (404) + The page you were looking for doesn't exist + -
+ Mastodon +
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

+

The page you were looking for doesn't exist

-

If you are the application owner check the logs for more information.

diff --git a/public/422.html b/public/422.html deleted file mode 100644 index eb3601e71a..0000000000 --- a/public/422.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 9b027daf8b..ab918fe508 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -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