From 829b774588944375922fed3f3d60d80d4483aeff Mon Sep 17 00:00:00 2001 From: June Rhodes Date: Sun, 27 Nov 2016 16:49:53 +1100 Subject: [PATCH] Implement data storage + UI behaviours to allow users to block domains --- .../components/actions/domains.jsx | 77 +++++++++++++++++++ .../account/components/action_bar.jsx | 14 +++- .../components/features/account/index.jsx | 24 +++++- .../javascripts/components/locales/en.jsx | 5 +- app/controllers/api/v1/accounts_controller.rb | 4 +- app/controllers/api/v1/domains_controller.rb | 27 +++++++ app/models/account.rb | 15 ++++ app/models/account_domain_block.rb | 12 +++ app/services/account_domain_block_service.rb | 7 ++ .../account_domain_unblock_service.rb | 7 ++ app/views/api/v1/accounts/relationship.rabl | 1 + app/views/api/v1/domains/index.rabl | 2 + app/views/api/v1/domains/show.rabl | 3 + config/routes.rb | 8 ++ ...1127152000_create_account_domain_blocks.rb | 12 +++ db/schema.rb | 10 ++- 16 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/components/actions/domains.jsx create mode 100644 app/controllers/api/v1/domains_controller.rb create mode 100644 app/models/account_domain_block.rb create mode 100644 app/services/account_domain_block_service.rb create mode 100644 app/services/account_domain_unblock_service.rb create mode 100644 app/views/api/v1/domains/index.rabl create mode 100644 app/views/api/v1/domains/show.rabl create mode 100644 db/migrate/20161127152000_create_account_domain_blocks.rb diff --git a/app/assets/javascripts/components/actions/domains.jsx b/app/assets/javascripts/components/actions/domains.jsx new file mode 100644 index 0000000000..ca41938fce --- /dev/null +++ b/app/assets/javascripts/components/actions/domains.jsx @@ -0,0 +1,77 @@ +import api, { getLinks } from '../api' +import Immutable from 'immutable'; + +export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; +export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; +export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; + +export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; +export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; +export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; + +export function blockDomain(domain) { + return (dispatch, getState) => { + dispatch(blockDomainRequest(domain)); + + api(getState).post(`/api/v1/domains/block?domain=${domain}`).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(blockDomainSuccess(response.data)); + }).catch(error => { + dispatch(blockDomainFail(domain, error)); + }); + }; +}; + +export function unblockDomain(domain) { + return (dispatch, getState) => { + dispatch(unblockDomainRequest(domain)); + + api(getState).post(`/api/v1/domains/unblock?domain=${domain}`).then(response => { + dispatch(unblockDomainSuccess(response.data)); + }).catch(error => { + dispatch(unblockDomainFail(domain, error)); + }); + }; +}; + +export function blockDomainRequest(id) { + return { + type: DOMAIN_BLOCK_REQUEST, + id + }; +}; + +export function blockDomainSuccess(blocked_domains) { + return { + type: DOMAIN_BLOCK_SUCCESS, + blocked_domains: blocked_domains + }; +}; + +export function blockDomainFail(error) { + return { + type: DOMAIN_BLOCK_FAIL, + error + }; +}; + +export function unblockDomainRequest(id) { + return { + type: DOMAIN_UNBLOCK_REQUEST, + id + }; +}; + +export function unblockDomainSuccess(blocked_domains) { + return { + type: DOMAIN_UNBLOCK_SUCCESS, + blocked_domains: blocked_domains + }; +}; + +export function unblockDomainFail(error) { + return { + type: DOMAIN_UNBLOCK_FAIL, + error + }; +}; \ No newline at end of file diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index f09dea6abf..06aeae82a7 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -8,10 +8,11 @@ const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }, + unblock_domain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - block: { id: 'account.block', defaultMessage: 'Block' }, + block: { id: 'account.block', defaultMessage: 'Block user' }, + block_domain: { id: 'account.block_domain', defaultMessage: 'Block domain' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - block: { id: 'account.block', defaultMessage: 'Block' } }); const outerStyle = { @@ -41,6 +42,7 @@ const ActionBar = React.createClass({ me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, onBlock: React.PropTypes.func.isRequired, + onBlockDomain: React.PropTypes.func.isRequired, onMention: React.PropTypes.func.isRequired }, @@ -55,6 +57,8 @@ const ActionBar = React.createClass({ if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + } else if (account.getIn(['relationship', 'blocking_domain'])) { + // Do not show per account block / unblock if the whole domain is filtered. } else if (account.getIn(['relationship', 'blocking'])) { menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock }); } else if (account.getIn(['relationship', 'following'])) { @@ -63,6 +67,12 @@ const ActionBar = React.createClass({ menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock }); } + if (account.getIn(['relationship', 'blocking_domain'])) { + menu.push({ text: intl.formatMessage(messages.unblock_domain), action: this.props.onBlockDomain }); + } else if (account.get('acct').indexOf('@') != -1 /* Is this account from an external instance? */) { + menu.push({ text: intl.formatMessage(messages.block_domain), action: this.props.onBlockDomain }); + } + return (
diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index c2cc58bb29..572597f76c 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -10,6 +10,10 @@ import { fetchAccountTimeline, expandAccountTimeline } from '../../actions/accounts'; +import { + blockDomain, + unblockDomain +} from '../../actions/domains'; import { mentionCompose } from '../../actions/compose'; import Header from './components/header'; import { @@ -69,6 +73,24 @@ const Account = React.createClass({ } }, + handleBlockDomain () { + let domain = null; + const acct = this.props.account.get('acct'); + if (acct.indexOf('@') == -1) { + // same domain as current user... ? + // we should not hit here because the UI should not show the option. + return; + } + + domain = acct.substring(acct.indexOf('@') + 1); + + if (this.props.account.getIn(['relationship', 'blocking_domain'])) { + this.props.dispatch(unblockDomain(domain)); + } else { + this.props.dispatch(blockDomain(domain)); + } + }, + handleMention () { this.props.dispatch(mentionCompose(this.props.account)); }, @@ -88,7 +110,7 @@ const Account = React.createClass({
- + {this.props.children} diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 41a44e3dcb..27bb268ec9 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -14,10 +14,11 @@ const en = { "account.mention": "Mention", "account.edit_profile": "Edit profile", "account.unblock": "Unblock", + "account.unblock_domain": "Unblock domain", "account.unfollow": "Unfollow", - "account.block": "Block", + "account.block": "Block user", + "account.block_domain": "Block domain", "account.follow": "Follow", - "account.block": "Block", "account.posts": "Posts", "account.follows": "Follows", "account.followers": "Followers", diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ffa8b04fb9..961c0e71fe 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -85,10 +85,11 @@ class Api::V1::AccountsController < ApiController def relationships ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] - @accounts = Account.where(id: ids).select('id') + @accounts = Account.where(id: ids).select('id, domain') @following = Account.following_map(ids, current_user.account_id) @followed_by = Account.followed_by_map(ids, current_user.account_id) @blocking = Account.blocking_map(ids, current_user.account_id) + @blocking_domain = Account.blocking_domains_map(Account.where(id: ids).select('domain'), current_user.account_id) end def search @@ -110,6 +111,7 @@ class Api::V1::AccountsController < ApiController @following = Account.following_map([@account.id], current_user.account_id) @followed_by = Account.followed_by_map([@account.id], current_user.account_id) @blocking = Account.blocking_map([@account.id], current_user.account_id) + @blocking_domain = Account.blocking_domains_map([@account.domain], current_user.account_id) end def cache(raw) diff --git a/app/controllers/api/v1/domains_controller.rb b/app/controllers/api/v1/domains_controller.rb new file mode 100644 index 0000000000..9cf0361e08 --- /dev/null +++ b/app/controllers/api/v1/domains_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Api::V1::DomainsController < ApiController + before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :write } + before_action :require_user! + + respond_to :json + + def blocks + @domains = AccountDomainBlock.where(account: current_account) + end + + def block + AccountDomainBlockService.new.call(current_user.account, params[:domain]) + + @domains = AccountDomainBlock.where(account: current_account) + render action: :index + end + + def unblock + AccountDomainUnblockService.new.call(current_user.account, params[:domain]) + + @domains = AccountDomainBlock.where(account: current_account) + render action: :index + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 65fad2f475..2fbb5b299e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -43,6 +43,8 @@ class Account < ApplicationRecord # Block relationships has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account + has_many :block_domain_relationships, class_name: 'AccountDomainBlock', foreign_key: 'account_id', dependent: :destroy + has_many :blocked_domains, -> { order('account_domain_blocks.id desc') }, through: :block_domain_relationships, source: :domain has_many :media_attachments, dependent: :destroy @@ -64,6 +66,10 @@ class Account < ApplicationRecord block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) end + def block_domain!(domain) + block_domain_relationships.where(domain: domain).first_or_create!(domain: domain) + end + def unfollow!(other_account) follow = active_relationships.find_by(target_account: other_account) follow&.destroy @@ -74,6 +80,11 @@ class Account < ApplicationRecord block&.destroy end + def unblock_domain!(domain) + block = block_domain_relationships.find_by(domain: domain) + block&.destroy + end + def following?(other_account) following.include?(other_account) end @@ -166,6 +177,10 @@ class Account < ApplicationRecord def blocking_map(target_account_ids, account_id) Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h end + + def blocking_domains_map(domains, account_id) + AccountDomainBlock.where(domain: domains).where(account_id: account_id).map { |d| [d.domain, true] }.to_h + end end before_create do diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb new file mode 100644 index 0000000000..bb3c954140 --- /dev/null +++ b/app/models/account_domain_block.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AccountDomainBlock < ApplicationRecord + belongs_to :account + validates :domain, presence: true, uniqueness: true + + validates :account_id, uniqueness: { scope: :domain } + + def self.blocked?(account, domain) + where(domain: domain, account: account).exists? + end +end diff --git a/app/services/account_domain_block_service.rb b/app/services/account_domain_block_service.rb new file mode 100644 index 0000000000..e6ade929cc --- /dev/null +++ b/app/services/account_domain_block_service.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AccountDomainBlockService < BaseService + def call(account, target_domain) + account.block_domain!(target_domain) + end +end diff --git a/app/services/account_domain_unblock_service.rb b/app/services/account_domain_unblock_service.rb new file mode 100644 index 0000000000..d6113fbfcb --- /dev/null +++ b/app/services/account_domain_unblock_service.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AccountDomainUnblockService < BaseService + def call(account, target_domain) + account.unblock_domain!(target_domain) + end +end diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl index 84043e9cd3..4752b30177 100644 --- a/app/views/api/v1/accounts/relationship.rabl +++ b/app/views/api/v1/accounts/relationship.rabl @@ -4,3 +4,4 @@ attribute :id node(:following) { |account| @following[account.id] || false } node(:followed_by) { |account| @followed_by[account.id] || false } node(:blocking) { |account| @blocking[account.id] || false } +node(:blocking_domain) { |account| @blocking_domain[account.domain] || false } \ No newline at end of file diff --git a/app/views/api/v1/domains/index.rabl b/app/views/api/v1/domains/index.rabl new file mode 100644 index 0000000000..8bd4bec3f2 --- /dev/null +++ b/app/views/api/v1/domains/index.rabl @@ -0,0 +1,2 @@ +collection @domains +extends('api/v1/domains/show') \ No newline at end of file diff --git a/app/views/api/v1/domains/show.rabl b/app/views/api/v1/domains/show.rabl new file mode 100644 index 0000000000..af092836a0 --- /dev/null +++ b/app/views/api/v1/domains/show.rabl @@ -0,0 +1,3 @@ +object @domain + +attributes :id, :domain, :account_id \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e0c14b47a1..c88559ade7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,6 +76,14 @@ Rails.application.routes.draw do resources :notifications, only: [:index] + resources :domains, only: [] do + collection do + get :blocks + post :block + post :unblock + end + end + resources :accounts, only: [:show] do collection do get :relationships diff --git a/db/migrate/20161127152000_create_account_domain_blocks.rb b/db/migrate/20161127152000_create_account_domain_blocks.rb new file mode 100644 index 0000000000..a81064a18c --- /dev/null +++ b/db/migrate/20161127152000_create_account_domain_blocks.rb @@ -0,0 +1,12 @@ +class CreateAccountDomainBlocks < ActiveRecord::Migration[5.0] + def change + create_table :account_domain_blocks do |t| + t.integer :account_id, null: false + t.string :domain, null: false, default: '' + + t.timestamps null: false + end + + add_index :account_domain_blocks, [:account_id, :domain], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 356badf8e6..70a2a7bff3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161123093447) do +ActiveRecord::Schema.define(version: 20161127152000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "account_domain_blocks", force: :cascade do |t| + t.integer "account_id", null: false + t.string "domain", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true, using: :btree + end + create_table "accounts", force: :cascade do |t| t.string "username", default: "", null: false t.string "domain"