Browse Source

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
master
Eugen Rochko 7 years ago
parent
commit
80e02b90e4
17 changed files with 106 additions and 149 deletions
  1. +1
    -1
      app/assets/javascripts/components/actions/compose.jsx
  2. +2
    -2
      app/controllers/accounts_controller.rb
  3. +2
    -3
      app/controllers/api/v1/accounts_controller.rb
  4. +2
    -1
      app/controllers/api/v1/statuses_controller.rb
  5. +3
    -3
      app/controllers/stream_entries_controller.rb
  6. +22
    -0
      app/models/block.rb
  7. +5
    -1
      app/models/concerns/streamable.rb
  8. +22
    -7
      app/models/status.rb
  9. +3
    -2
      app/models/stream_entry.rb
  10. +1
    -1
      app/services/post_status_service.rb
  11. +2
    -0
      app/services/reblog_service.rb
  12. +3
    -3
      app/views/api/v1/statuses/_show.rabl
  13. +5
    -0
      db/migrate/20161221152630_add_hidden_to_stream_entries.rb
  14. +4
    -3
      db/schema.rb
  15. +25
    -50
      public/404.html
  16. +0
    -68
      public/422.html
  17. +4
    -4
      spec/controllers/api/v1/statuses_controller_spec.rb

+ 1
- 1
app/assets/javascripts/components/actions/compose.jsx View File

@@ -67,7 +67,7 @@ export function submitCompose() {
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']), sensitive: getState().getIn(['compose', 'sensitive']),
unlisted: getState().getIn(['compose', 'unlisted'])
visibility: getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public'
}).then(function (response) { }).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));




+ 2
- 2
app/controllers/accounts_controller.rb View File

@@ -11,12 +11,12 @@ class AccountsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do 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) @statuses = cache_collection(@statuses, Status)
end end


format.atom do 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 end
end end


+ 2
- 3
app/controllers/api/v1/accounts_controller.rb View File

@@ -8,8 +8,7 @@ class Api::V1::AccountsController < ApiController


respond_to :json respond_to :json


def show
end
def show; end


def verify_credentials def verify_credentials
@account = current_user.account @account = current_user.account
@@ -47,7 +46,7 @@ class Api::V1::AccountsController < ApiController
end end


def statuses 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) @statuses = cache_collection(@statuses, Status)


set_maps(@statuses) set_maps(@statuses)


+ 2
- 1
app/controllers/api/v1/statuses_controller.rb View File

@@ -52,7 +52,7 @@ class Api::V1::StatusesController < ApiController
end end


def create 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 render action: :show
end end


@@ -95,5 +95,6 @@ class Api::V1::StatusesController < ApiController


def set_status def set_status
@status = Status.find(params[:id]) @status = Status.find(params[:id])
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
end end
end end

+ 3
- 3
app/controllers/stream_entries_controller.rb View File

@@ -14,8 +14,8 @@ class StreamEntriesController < ApplicationController
return gone if @stream_entry.activity.nil? return gone if @stream_entry.activity.nil?


if @stream_entry.activity_type == 'Status' 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
end end


@@ -43,7 +43,7 @@ class StreamEntriesController < ApplicationController
end end


def set_stream_entry 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 @type = @stream_entry.activity_type.downcase
end end




+ 22
- 0
app/models/block.rb View File

@@ -1,9 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true


class Block < ApplicationRecord class Block < ApplicationRecord
include Streamable

belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'


validates :account, :target_account, presence: true validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } 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 end

+ 5
- 1
app/models/concerns/streamable.rb View File

@@ -26,8 +26,12 @@ module Streamable
super super
end end


def hidden?
false
end

after_create do 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 end
end end

+ 22
- 7
app/models/status.rb View File

@@ -5,7 +5,7 @@ class Status < ApplicationRecord
include Streamable include Streamable
include Cacheable include Cacheable


enum visibility: [:public, :unlisted], _suffix: :visibility
enum visibility: [:public, :unlisted, :private], _suffix: :visibility


belongs_to :account, inverse_of: :statuses belongs_to :account, inverse_of: :statuses


@@ -66,19 +66,19 @@ class Status < ApplicationRecord
content content
end end


def reblogs_count
attributes['reblogs_count'] || reblogs.count
def hidden?
private_visibility?
end 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 end


def ancestors(account = nil) 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) 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) statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first } 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 results
end 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) 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) statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first } 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 results
end 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 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 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) def reload_stale_associations!(cached_items)
account_ids = [] account_ids = []


@@ -161,5 +169,12 @@ class Status < ApplicationRecord
before_validation do before_validation do
text.strip! text.strip!
self.in_reply_to_account_id = thread.account_id if reply? 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
end end

+ 3
- 2
app/models/stream_entry.rb View File

@@ -9,6 +9,7 @@ class StreamEntry < ApplicationRecord
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id' belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id'
belongs_to :follow, foreign_type: 'Follow', 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 :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id'
belongs_to :block, foreign_type: 'Block', foreign_key: 'activity_id'


validates :account, :activity, presence: true validates :account, :activity, presence: true


@@ -29,7 +30,7 @@ class StreamEntry < ApplicationRecord
end end


def targeted? def targeted?
[:follow, :share, :favorite].include? verb
[:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
end end


def target def target
@@ -57,7 +58,7 @@ class StreamEntry < ApplicationRecord
end end


def activity def activity
send(activity_type.downcase.to_sym)
!new_record? ? send(activity_type.downcase) : super
end end


private private


+ 1
- 1
app/services/post_status_service.rb View File

@@ -10,7 +10,7 @@ class PostStatusService < BaseService
# @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Enumerable] :media_ids Optional array of media IDs to attach
# @return [Status] # @return [Status]
def call(account, text, in_reply_to = nil, options = {}) 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]) attach_media(status, options[:media_ids])
process_mentions_service.call(status) process_mentions_service.call(status)
process_hashtags_service.call(status) process_hashtags_service.call(status)


+ 2
- 0
app/services/reblog_service.rb View File

@@ -6,6 +6,8 @@ class ReblogService < BaseService
# @param [Status] reblogged_status Status to be reblogged # @param [Status] reblogged_status Status to be reblogged
# @return [Status] # @return [Status]
def call(account, reblogged_status) def call(account, reblogged_status)
raise ActiveRecord::RecordInvalid if reblogged_status.private_visibility?

reblog = account.statuses.create!(reblog: reblogged_status, text: '') reblog = account.statuses.create!(reblog: reblogged_status, text: '')


DistributionWorker.perform_async(reblog.id) DistributionWorker.perform_async(reblog.id)


+ 3
- 3
app/views/api/v1/statuses/_show.rabl 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(:uri) { |status| TagManager.instance.uri_for(status) }
node(:content) { |status| Formatter.instance.format(status) } node(:content) { |status| Formatter.instance.format(status) }
node(:url) { |status| TagManager.instance.url_for(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 child :account do
extends 'api/v1/accounts/show' extends 'api/v1/accounts/show'


+ 5
- 0
db/migrate/20161221152630_add_hidden_to_stream_entries.rb View File

@@ -0,0 +1,5 @@
class AddHiddenToStreamEntries < ActiveRecord::Migration[5.0]
def change
add_column :stream_entries, :hidden, :boolean, null: false, default: false
end
end

+ 4
- 3
db/schema.rb View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -196,8 +196,9 @@ ActiveRecord::Schema.define(version: 20161205214545) do
t.integer "account_id" t.integer "account_id"
t.integer "activity_id" t.integer "activity_id"
t.string "activity_type" 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 ["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 t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree
end end


+ 25
- 50
public/404.html View File

@@ -2,67 +2,42 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <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"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto:400" rel="stylesheet">
<style> <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> </style>
</head> </head>


<body> <body>
<!-- This file lives in public/404.html -->
<div class="dialog"> <div class="dialog">
<img src="/oops.png" alt="Mastodon" />

<div> <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> </div>
<p>If you are the application owner check the logs for more information.</p>
</div> </div>
</body> </body>
</html> </html>

+ 0
- 68
public/422.html View File

@@ -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>

+ 4
- 4
spec/controllers/api/v1/statuses_controller_spec.rb View File

@@ -97,7 +97,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end end


it 'updates the reblogs count' do it 'updates the reblogs count' do
expect(status.reblogs_count).to eq 1
expect(status.reblogs.count).to eq 1
end end


it 'updates the reblogged attribute' do it 'updates the reblogged attribute' do
@@ -126,7 +126,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end end


it 'updates the reblogs count' do it 'updates the reblogs count' do
expect(status.reblogs_count).to eq 0
expect(status.reblogs.count).to eq 0
end end


it 'updates the reblogged attribute' do it 'updates the reblogged attribute' do
@@ -146,7 +146,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end end


it 'updates the favourites count' do it 'updates the favourites count' do
expect(status.favourites_count).to eq 1
expect(status.favourites.count).to eq 1
end end


it 'updates the favourited attribute' do it 'updates the favourited attribute' do
@@ -175,7 +175,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end end


it 'updates the favourites count' do it 'updates the favourites count' do
expect(status.favourites_count).to eq 0
expect(status.favourites.count).to eq 0
end end


it 'updates the favourited attribute' do it 'updates the favourited attribute' do


Loading…
Cancel
Save