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
This commit is contained in:
Eugen Rochko 2016-12-21 20:00:18 +01:00
parent 6d71044c85
commit 80e02b90e4
17 changed files with 106 additions and 149 deletions

View 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 }));

View 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

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View 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)

View 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)

View 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'