From cdb101340a20183a82889f811d9311c370c855e5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 29 Jun 2018 15:34:36 +0200 Subject: Keyword/phrase filtering (#7905) * Add keyword filtering GET|POST /api/v1/filters GET|PUT|DELETE /api/v1/filters/:id - Irreversible filters can drop toots from home or notifications - Other filters can hide toots through the client app - Filters use a phrase valid in particular contexts, expiration * Make sure expired filters don't get applied client-side * Add missing API methods * Remove "regex filter" from column settings * Add tests * Add test for FeedManager * Add CustomFilter test * Add UI for managing filters * Add streaming API event to allow syncing filters * Fix tests --- spec/controllers/api/v1/filter_controller_spec.rb | 81 +++++++++++++++++++++++ spec/fabricators/custom_filter_fabricator.rb | 6 ++ spec/lib/feed_manager_spec.rb | 8 +++ spec/models/custom_filter_spec.rb | 5 ++ 4 files changed, 100 insertions(+) create mode 100644 spec/controllers/api/v1/filter_controller_spec.rb create mode 100644 spec/fabricators/custom_filter_fabricator.rb create mode 100644 spec/models/custom_filter_spec.rb (limited to 'spec') diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filter_controller_spec.rb new file mode 100644 index 000000000..3ffd8f784 --- /dev/null +++ b/spec/controllers/api/v1/filter_controller_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +RSpec.describe Api::V1::FiltersController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + let!(:filter) { Fabricate(:custom_filter, account: user.account) } + + it 'returns http success' do + get :index + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + before do + post :create, params: { phrase: 'magic', context: %w(home), irreversible: true } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates a filter' do + filter = user.account.custom_filters.first + expect(filter).to_not be_nil + expect(filter.phrase).to eq 'magic' + expect(filter.context).to eq %w(home) + expect(filter.irreversible?).to be true + expect(filter.expires_at).to be_nil + end + end + + describe 'GET #show' do + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + it 'returns http success' do + get :show, params: { id: filter.id } + expect(response).to have_http_status(200) + end + end + + describe 'PUT #update' do + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + before do + put :update, params: { id: filter.id, phrase: 'updated' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'updates the filter' do + expect(filter.reload.phrase).to eq 'updated' + end + end + + describe 'DELETE #destroy' do + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + before do + delete :destroy, params: { id: filter.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes the filter' do + expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound + end + end +end diff --git a/spec/fabricators/custom_filter_fabricator.rb b/spec/fabricators/custom_filter_fabricator.rb new file mode 100644 index 000000000..64297a7e3 --- /dev/null +++ b/spec/fabricators/custom_filter_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:custom_filter) do + account + expires_at nil + phrase 'discourse' + context %w(home notifications) +end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 6ead5bbd9..d1b847675 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -126,6 +126,14 @@ RSpec.describe FeedManager do reblog = Fabricate(:status, reblog: status, account: jeff) expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end + + it 'returns true if status contains irreversibly muted phrase' do + alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true) + alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true) + alice.follow!(jeff) + status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff) + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end end context 'for mentions feed' do diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb new file mode 100644 index 000000000..1024542e7 --- /dev/null +++ b/spec/models/custom_filter_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe CustomFilter, type: :model do + +end -- cgit From da8fe8079e13758f45e5ba77cb8023c554ae193c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 3 Jul 2018 01:47:56 +0200 Subject: Re-add follow recommendations API (#7918) * Re-add follow recommendations API GET /api/v1/suggestions Removed in 8efa081f210d72ed450c39ac4cde0fd84fb3d3fb due to Neo4J dependency. The algorithm uses triadic closures, takes into account suspensions, blocks, mutes, domain blocks, excludes locked and moved accounts, and prefers more recently updated accounts. * Track interactions with people you don't follow Replying to, favouriting and reblogging someone you're not following will make them show up in follow recommendations. The interactions have different weights: - Replying is 1 - Favouriting is 10 (decidedly positive interaction, but private) - Reblogging is 20 Following them, muting or blocking will remove them from the list, obviously. * Remove triadic closures, ensure potential friendships are trimmed --- app/controllers/api/v1/suggestions_controller.rb | 21 +++++++ app/lib/potential_friendship_tracker.rb | 39 ++++++++++++ app/models/account.rb | 29 +-------- app/models/concerns/account_interactions.rb | 12 ++++ app/services/favourite_service.rb | 8 +++ app/services/post_status_service.rb | 7 +++ app/services/reblog_service.rb | 7 +++ config/routes.rb | 1 + .../api/v1/suggestions_controller_spec.rb | 35 +++++++++++ spec/models/account_spec.rb | 71 ---------------------- 10 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 app/controllers/api/v1/suggestions_controller.rb create mode 100644 app/lib/potential_friendship_tracker.rb create mode 100644 spec/controllers/api/v1/suggestions_controller_spec.rb (limited to 'spec') diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb new file mode 100644 index 000000000..3abccedd5 --- /dev/null +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Api::V1::SuggestionsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :read } + before_action :require_user! + before_action :set_accounts + + respond_to :json + + def index + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def set_accounts + @accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) + end +end diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb new file mode 100644 index 000000000..362482669 --- /dev/null +++ b/app/lib/potential_friendship_tracker.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class PotentialFriendshipTracker + EXPIRE_AFTER = 90.days.seconds + MAX_ITEMS = 80 + + WEIGHTS = { + reply: 1, + favourite: 10, + reblog: 20, + }.freeze + + class << self + def record(account_id, target_account_id, action) + key = "interactions:#{account_id}" + weight = WEIGHTS[action] + + redis.zincrby(key, weight, target_account_id) + redis.zremrangebyrank(key, 0, -MAX_ITEMS) + redis.expire(key, EXPIRE_AFTER) + end + + def remove(account_id, target_account_id) + redis.zrem("interactions:#{account_id}", target_account_id) + end + + def get(account_id, limit: 20, offset: 0) + account_ids = redis.zrevrange("interactions:#{account_id}", offset, limit) + return [] if account_ids.empty? + Account.searchable.where(id: account_ids) + end + + private + + def redis + Redis.current + end + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 40a45b1f8..1f720bf88 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -127,6 +127,7 @@ class Account < ApplicationRecord scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) } delegate :email, :unconfirmed_email, @@ -309,34 +310,6 @@ class Account < ApplicationRecord DeliveryFailureTracker.filter(urls) end - def triadic_closures(account, limit: 5, offset: 0) - sql = <<-SQL.squish - WITH first_degree AS ( - SELECT target_account_id - FROM follows - WHERE account_id = :account_id - ) - SELECT accounts.* - FROM follows - INNER JOIN accounts ON follows.target_account_id = accounts.id - WHERE - account_id IN (SELECT * FROM first_degree) - AND target_account_id NOT IN (SELECT * FROM first_degree) - AND target_account_id NOT IN (:excluded_account_ids) - AND accounts.suspended = false - GROUP BY target_account_id, accounts.id - ORDER BY count(account_id) DESC - OFFSET :offset - LIMIT :limit - SQL - - excluded_account_ids = account.excluded_from_timeline_account_ids + [account.id] - - find_by_sql( - [sql, { account_id: account.id, excluded_account_ids: excluded_account_ids, limit: limit, offset: offset }] - ) - end - def search_for(terms, limit = 10) textsearch, query = generate_query_for_search(terms) diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index ef59f5d15..ee435f956 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -89,10 +89,13 @@ module AccountInteractions .find_or_create_by!(target_account: other_account) rel.update!(show_reblogs: reblogs) + remove_potential_friendship(other_account) + rel end def block!(other_account, uri: nil) + remove_potential_friendship(other_account) block_relationships.create_with(uri: uri) .find_or_create_by!(target_account: other_account) end @@ -100,10 +103,13 @@ module AccountInteractions def mute!(other_account, notifications: nil) notifications = true if notifications.nil? mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account) + remove_potential_friendship(other_account) + # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't. if mute.hide_notifications? != notifications mute.update!(hide_notifications: notifications) end + mute end @@ -194,4 +200,10 @@ module AccountInteractions lists.joins(account: :user) .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) end + + private + + def remove_potential_friendship(other_account) + PotentialFriendshipTracker.remove(id, other_account.id) + end end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index bc2d1547a..6e1ac3ba9 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -15,7 +15,10 @@ class FavouriteService < BaseService return favourite unless favourite.nil? favourite = Favourite.create!(account: account, status: status) + create_notification(favourite) + bump_potential_friendship(account, status) + favourite end @@ -33,6 +36,11 @@ class FavouriteService < BaseService end end + def bump_potential_friendship(account, status) + return if account.following?(status.account_id) + PotentialFriendshipTracker.record(account.id, status.account_id, :favourite) + end + def build_json(favourite) Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( favourite, diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 735985725..bad82051a 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -47,6 +47,8 @@ class PostStatusService < BaseService redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id) end + bump_potential_friendship(account, status) + status end @@ -79,4 +81,9 @@ class PostStatusService < BaseService def redis Redis.current end + + def bump_potential_friendship(account, status) + return if !status.reply? || account.following?(status.account_id) + PotentialFriendshipTracker.record(account.id, status.in_reply_to_account_id, :reply) + end end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 3c4e5847f..0ee8bac2f 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -24,6 +24,8 @@ class ReblogService < BaseService ActivityPub::DistributionWorker.perform_async(reblog.id) create_notification(reblog) + bump_potential_friendship(account, reblog) + reblog end @@ -41,6 +43,11 @@ class ReblogService < BaseService end end + def bump_potential_friendship(account, reblog) + return if account.following?(reblog.reblog.account_id) + PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog) + end + def build_json(reblog) Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( reblog, diff --git a/config/routes.rb b/config/routes.rb index 5fdd3b390..e59325964 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -246,6 +246,7 @@ Rails.application.routes.draw do resources :streaming, only: [:index] resources :custom_emojis, only: [:index] + resources :suggestions, only: [:index] get '/search', to: 'search#index', as: :search diff --git a/spec/controllers/api/v1/suggestions_controller_spec.rb b/spec/controllers/api/v1/suggestions_controller_spec.rb new file mode 100644 index 000000000..17f10b04f --- /dev/null +++ b/spec/controllers/api/v1/suggestions_controller_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe Api::V1::SuggestionsController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + let(:bob) { Fabricate(:account) } + let(:jeff) { Fabricate(:account) } + + before do + PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog) + PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite) + + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns accounts' do + json = body_as_json + + expect(json.size).to be >= 1 + expect(json.map { |i| i[:id] }).to include *[bob, jeff].map { |i| i.id.to_s } + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index cce659a8a..c50791bcd 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -454,77 +454,6 @@ RSpec.describe Account, type: :model do end end - describe '.triadic_closures' do - let!(:me) { Fabricate(:account) } - let!(:friend) { Fabricate(:account) } - let!(:friends_friend) { Fabricate(:account) } - let!(:both_follow) { Fabricate(:account) } - - before do - me.follow!(friend) - friend.follow!(friends_friend) - - me.follow!(both_follow) - friend.follow!(both_follow) - end - - it 'finds accounts you dont follow which are followed by accounts you do follow' do - expect(described_class.triadic_closures(me)).to eq [friends_friend] - end - - it 'limits by 5 with offset 0 by defualt' do - first_degree = 6.times.map { Fabricate(:account) } - matches = 5.times.map { Fabricate(:account) } - first_degree.each { |account| me.follow!(account) } - matches.each do |match| - first_degree.each { |account| account.follow!(match) } - first_degree.shift - end - - expect(described_class.triadic_closures(me)).to eq matches - end - - it 'accepts arbitrary limits' do - another_friend = Fabricate(:account) - higher_friends_friend = Fabricate(:account) - me.follow!(another_friend) - friend.follow!(higher_friends_friend) - another_friend.follow!(higher_friends_friend) - - expect(described_class.triadic_closures(me, limit: 1)).to eq [higher_friends_friend] - end - - it 'acceps arbitrary offset' do - another_friend = Fabricate(:account) - higher_friends_friend = Fabricate(:account) - me.follow!(another_friend) - friend.follow!(higher_friends_friend) - another_friend.follow!(higher_friends_friend) - - expect(described_class.triadic_closures(me, offset: 1)).to eq [friends_friend] - end - - context 'when you block account' do - before do - me.block!(friends_friend) - end - - it 'rejects blocked accounts' do - expect(described_class.triadic_closures(me)).to be_empty - end - end - - context 'when you mute account' do - before do - me.mute!(friends_friend) - end - - it 'rejects muted accounts' do - expect(described_class.triadic_closures(me)).to be_empty - end - end - end - describe '#statuses_count' do subject { Fabricate(:account) } -- cgit From 1f6ed4f86ab2aa98bb271b40bf381370fab4fdf2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 5 Jul 2018 18:31:35 +0200 Subject: Add more granular OAuth scopes (#7929) * Add more granular OAuth scopes * Add human-readable descriptions of the new scopes * Ensure new scopes look good on the app UI * Add tests * Group scopes in screen and color-code dangerous ones * Fix wrong extra scope --- app/controllers/api/base_controller.rb | 4 ++ .../api/v1/accounts/credentials_controller.rb | 4 +- .../v1/accounts/follower_accounts_controller.rb | 2 +- .../v1/accounts/following_accounts_controller.rb | 2 +- .../api/v1/accounts/lists_controller.rb | 2 +- .../api/v1/accounts/relationships_controller.rb | 2 +- .../api/v1/accounts/search_controller.rb | 2 +- .../api/v1/accounts/statuses_controller.rb | 2 +- app/controllers/api/v1/accounts_controller.rb | 7 +++- app/controllers/api/v1/blocks_controller.rb | 2 +- app/controllers/api/v1/domain_blocks_controller.rb | 3 +- app/controllers/api/v1/favourites_controller.rb | 2 +- app/controllers/api/v1/filters_controller.rb | 4 +- .../api/v1/follow_requests_controller.rb | 3 +- app/controllers/api/v1/follows_controller.rb | 2 +- .../api/v1/lists/accounts_controller.rb | 4 +- app/controllers/api/v1/lists_controller.rb | 4 +- app/controllers/api/v1/media_controller.rb | 2 +- app/controllers/api/v1/mutes_controller.rb | 2 +- app/controllers/api/v1/notifications_controller.rb | 3 +- app/controllers/api/v1/reports_controller.rb | 4 +- app/controllers/api/v1/search_controller.rb | 2 +- .../statuses/favourited_by_accounts_controller.rb | 7 +--- .../api/v1/statuses/favourites_controller.rb | 2 +- .../api/v1/statuses/mutes_controller.rb | 2 +- app/controllers/api/v1/statuses/pins_controller.rb | 2 +- .../statuses/reblogged_by_accounts_controller.rb | 7 +--- .../api/v1/statuses/reblogs_controller.rb | 2 +- app/controllers/api/v1/statuses_controller.rb | 9 +---- .../api/v1/timelines/direct_controller.rb | 2 +- .../api/v1/timelines/home_controller.rb | 2 +- .../api/v1/timelines/list_controller.rb | 2 +- app/helpers/application_helper.rb | 10 +++++ app/javascript/styles/mastodon/forms.scss | 4 ++ app/views/settings/applications/_fields.html.haml | 17 +++----- config/initializers/doorkeeper.rb | 27 ++++++++++++- config/locales/doorkeeper.en.yml | 30 +++++++++++++-- config/locales/simple_form.en.yml | 1 + .../api/v1/accounts/credentials_controller_spec.rb | 6 ++- .../accounts/follower_accounts_controller_spec.rb | 2 +- .../accounts/following_accounts_controller_spec.rb | 2 +- .../api/v1/accounts/lists_controller_spec.rb | 2 +- .../v1/accounts/relationships_controller_spec.rb | 2 +- .../api/v1/accounts/search_controller_spec.rb | 2 +- .../api/v1/accounts/statuses_controller_spec.rb | 2 +- .../controllers/api/v1/accounts_controller_spec.rb | 45 ++++++++++++++++++++-- spec/controllers/api/v1/blocks_controller_spec.rb | 14 ++++++- .../api/v1/domain_blocks_controller_spec.rb | 22 ++++++++++- .../api/v1/favourites_controller_spec.rb | 2 +- spec/controllers/api/v1/filter_controller_spec.rb | 8 +++- .../api/v1/follow_requests_controller_spec.rb | 8 +++- spec/controllers/api/v1/follows_controller_spec.rb | 2 +- .../api/v1/lists/accounts_controller_spec.rb | 7 +++- spec/controllers/api/v1/lists_controller_spec.rb | 12 +++++- spec/controllers/api/v1/media_controller_spec.rb | 2 +- spec/controllers/api/v1/mutes_controller_spec.rb | 2 +- .../api/v1/notifications_controller_spec.rb | 10 ++++- spec/controllers/api/v1/reports_controller_spec.rb | 5 ++- spec/controllers/api/v1/search_controller_spec.rb | 2 +- .../favourited_by_accounts_controller_spec.rb | 2 +- .../api/v1/statuses/favourites_controller_spec.rb | 2 +- .../api/v1/statuses/mutes_controller_spec.rb | 2 +- .../api/v1/statuses/pins_controller_spec.rb | 2 +- .../reblogged_by_accounts_controller_spec.rb | 2 +- .../api/v1/statuses/reblogs_controller_spec.rb | 2 +- .../controllers/api/v1/statuses_controller_spec.rb | 7 +++- .../api/v1/timelines/home_controller_spec.rb | 2 +- .../api/v1/timelines/list_controller_spec.rb | 2 +- spec/controllers/api/v2/search_controller_spec.rb | 22 +++++++++++ 69 files changed, 292 insertions(+), 102 deletions(-) create mode 100644 spec/controllers/api/v2/search_controller_spec.rb (limited to 'spec') diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b5c084e14..770a69921 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -78,4 +78,8 @@ class Api::BaseController < ApplicationController def render_empty render json: {}, status: 200 end + + def authorize_if_got_token!(*scopes) + doorkeeper_authorize!(*scopes) if doorkeeper_token + end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 2d0737ee4..dcd41b35c 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, except: [:update] - before_action -> { doorkeeper_authorize! :write }, only: [:update] + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update] + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action :require_user! def show diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 4578cf6ca..daa35769e 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowerAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index ce2bbda85..6be97b87e 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowingAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb index a7ba89ce2..72392453c 100644 --- a/app/controllers/api/v1/accounts/lists_controller.rb +++ b/app/controllers/api/v1/accounts/lists_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::ListsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:lists' } before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 70236d1a8..ab8a0461f 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::RelationshipsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:follows' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 7649da433..91c9f1547 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::SearchController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index c40155cb5..06fa6c762 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::StatusesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b7133ca8e..1d5372a8c 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true class Api::V1::AccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] - before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow] + before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] + before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock] + before_action :require_user!, except: [:show] before_action :set_account before_action :check_account_suspension, only: [:show] diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a39701340..99c53d59a 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::BlocksController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow } + before_action -> { doorkeeper_authorize! :follow, :'read:blocks' } before_action :require_user! after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index e55d622c3..af9e7a20f 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -3,7 +3,8 @@ class Api::V1::DomainBlocksController < Api::BaseController BLOCK_LIMIT = 100 - before_action -> { doorkeeper_authorize! :follow } + before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }, only: :show + before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, except: :show before_action :require_user! after_action :insert_pagination_headers, only: :show diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index b4265ed34..ab5204355 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::FavouritesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:favourites' } before_action :require_user! after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index c89722b85..02efd323b 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, only: [:index, :show] - before_action -> { doorkeeper_authorize! :write }, except: [:index, :show] + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! before_action :set_filters, only: :index before_action :set_filter, only: [:show, :update, :destroy] diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index d5c7c565a..313fe2f81 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Api::V1::FollowRequestsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow } + before_action -> { doorkeeper_authorize! :follow, :'read:follows' }, only: :index + before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, except: :index before_action :require_user! after_action :insert_pagination_headers, only: :index diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index 5a2b2f32f..5420c0533 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::FollowsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow } + before_action -> { doorkeeper_authorize! :follow, :'write:follows' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index f2bded851..19de56732 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::Lists::AccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, only: [:show] - before_action -> { doorkeeper_authorize! :write }, except: [:show] + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] before_action :require_user! before_action :set_list diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 180a91d81..b42b8b971 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::ListsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, only: [:index, :show] - before_action -> { doorkeeper_authorize! :write }, except: [:index, :show] + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show] before_action :require_user! before_action :set_list, except: [:index, :create] diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index d4e6337e7..aaa93b615 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::MediaController < Api::BaseController - before_action -> { doorkeeper_authorize! :write } + before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! include ObfuscateFilename diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index c457408ba..faa7d16cd 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::MutesController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow } + before_action -> { doorkeeper_authorize! :follow, :'read:mutes' } before_action :require_user! after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index ebbe0b292..593c8f9a9 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Api::V1::NotificationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] + before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] before_action :require_user! after_action :insert_pagination_headers, only: :index diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index f5095e073..a954101cb 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::ReportsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, except: [:create] - before_action -> { doorkeeper_authorize! :write }, only: [:create] + before_action -> { doorkeeper_authorize! :read, :'read:reports' }, except: [:create] + before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create] before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 05754d0f2..dc1a37599 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -5,7 +5,7 @@ class Api::V1::SearchController < Api::BaseController RESULTS_LIMIT = 5 - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 3fe304153..8f4070bc7 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController include Authorization - before_action :authorize_if_got_token + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_status after_action :insert_pagination_headers @@ -71,11 +71,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController raise ActiveRecord::RecordNotFound end - def authorize_if_got_token - request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods) - doorkeeper_authorize! :read if request_token - end - def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 35f8a48cd..cceee9060 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :write } + before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index a4bf0acdd..b02469b4f 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :write } + before_action -> { doorkeeper_authorize! :write, :'write:mutes' } before_action :require_user! before_action :set_status before_action :set_conversation diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 54f8be667..4118a8ce4 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::PinsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :write } + before_action -> { doorkeeper_authorize! :write, :'write:accounts' } before_action :require_user! before_action :set_status diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index b065db2c7..93b83ce48 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController include Authorization - before_action :authorize_if_got_token + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_status after_action :insert_pagination_headers @@ -68,11 +68,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController raise ActiveRecord::RecordNotFound end - def authorize_if_got_token - request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods) - doorkeeper_authorize! :read if request_token - end - def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 634af474f..04847a6b7 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :write } + before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! respond_to :json diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 289d91045..c6925d462 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -3,8 +3,8 @@ class Api::V1::StatusesController < Api::BaseController include Authorization - before_action :authorize_if_got_token, except: [:create, :destroy] - before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy] + before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] + before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] before_action :require_user!, except: [:show, :context, :card] before_action :set_status, only: [:show, :context, :card] @@ -84,9 +84,4 @@ class Api::V1::StatusesController < Api::BaseController def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end - - def authorize_if_got_token - request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods) - doorkeeper_authorize! :read if request_token - end end diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb index ef64078be..d8a76d153 100644 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::DirectController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, only: [:show] + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] before_action :require_user!, only: [:show] after_action :insert_pagination_headers, unless: -> { @statuses.empty? } diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index cde4e8420..4412aaaa3 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::HomeController < Api::BaseController - before_action -> { doorkeeper_authorize! :read }, only: [:show] + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] before_action :require_user!, only: [:show] after_action :insert_pagination_headers, unless: -> { @statuses.empty? } diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index 06d596c08..cfc5f3b5e 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::ListController < Api::BaseController - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:lists' } before_action :require_user! before_action :set_list before_action :set_statuses diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 95863ab1f..327901e4e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true module ApplicationHelper + DANGEROUS_SCOPES = %w( + read + write + follow + ).freeze + def active_nav_class(path) current_page?(path) ? 'active' : '' end @@ -43,6 +49,10 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end + def class_for_scope(scope) + 'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s) + end + def can?(action, record) return false if record.nil? policy(record).public_send("#{action}?") diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index e4fd6c1f1..458eb86e9 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -612,3 +612,7 @@ code { display: block; } } + +.scope-danger { + color: $warning-red; +} diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml index b21f3cca6..db90df349 100644 --- a/app/views/settings/applications/_fields.html.haml +++ b/app/views/settings/applications/_fields.html.haml @@ -8,14 +8,9 @@ %p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: Doorkeeper.configuration.native_redirect_uri) .field-group - = f.input :scopes, - label: t('activerecord.attributes.doorkeeper/application.scopes'), - collection: Doorkeeper.configuration.scopes, - wrapper: :with_label, - include_blank: false, - label_method: lambda { |scope| safe_join([scope, content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, - selected: f.object.scopes.all, - required: false, - as: :check_boxes, - collection_wrapper_tag: 'ul', - item_wrapper_tag: 'li' + .input.with_block_label + %label= t('activerecord.attributes.doorkeeper/application.scopes') + %span.hint= t('simple_form.hints.defaults.scopes') + + - Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each do |k, v| + = f.input :scopes, label: false, hint: false, collection: v.sort, wrapper: :with_block_label, include_blank: false, label_method: lambda { |scope| safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, selected: f.object.scopes.all, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 469553803..fe2490b32 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -55,7 +55,32 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :read - optional_scopes :write, :follow, :push + optional_scopes :write, + :'write:accounts', + :'write:blocks', + :'write:favourites', + :'write:filters', + :'write:follows', + :'write:lists', + :'write:media', + :'write:mutes', + :'write:notifications', + :'write:reports', + :'write:statuses', + :read, + :'read:accounts', + :'read:blocks', + :'read:favourites', + :'read:filters', + :'read:follows', + :'read:lists', + :'read:mutes', + :'read:notifications', + :'read:reports', + :'read:search', + :'read:statuses', + :follow, + :push # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index eca1fc675..f1fe03716 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -114,7 +114,29 @@ en: application: title: OAuth authorization required scopes: - follow: follow, block, unblock and unfollow accounts - push: receive push notifications for your account - read: read your account's data - write: post on your behalf + follow: modify account relationships + push: receive your push notifications + read: read all your account's data + read:accounts: see accounts information + read:blocks: see your blocks + read:favourites: see your favourites + read:filters: see your filters + read:follows: see your follows + read:lists: see your lists + read:mutes: see your mutes + read:notifications: see your notifications + read:reports: see your reports + read:search: search on your behalf + read:statuses: see all statuses + write: modify all your account's data + write:accounts: modify your profile + write:blocks: block accounts and domains + write:favourites: favourite statuses + write:filters: create filters + write:follows: follow people + write:lists: create lists + write:media: upload media files + write:mutes: mute people and conversations + write:notifications: clear your notifications + write:reports: report other people + write:statuses: publish statuses diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 59133ea73..49d94bcde 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -20,6 +20,7 @@ en: one: 1 character left other: %{count} characters left phrase: Will be matched regardless of casing in text or content warning of a toot + scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_default_language: The language of your toots can be detected automatically, but it's not always accurate setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index 9a52fd14c..727669886 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::CredentialsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } context 'with an oauth token' do before do @@ -12,6 +12,8 @@ describe Api::V1::Accounts::CredentialsController do end describe 'GET #show' do + let(:scopes) { 'read:accounts' } + it 'returns http success' do get :show expect(response).to have_http_status(200) @@ -19,6 +21,8 @@ describe Api::V1::Accounts::CredentialsController do end describe 'PATCH #update' do + let(:scopes) { 'write:accounts' } + describe 'with valid data' do before do allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb index b47af4963..75e0570e9 100644 --- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowerAccountsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } before do Fabricate(:follow, target_account: user.account) diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb index 29fd7cd5b..7f7105ad3 100644 --- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowingAccountsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } before do Fabricate(:follow, account: user.account) diff --git a/spec/controllers/api/v1/accounts/lists_controller_spec.rb b/spec/controllers/api/v1/accounts/lists_controller_spec.rb index df9fe0e34..baafea8e6 100644 --- a/spec/controllers/api/v1/accounts/lists_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/lists_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::ListsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') } let(:account) { Fabricate(:account) } let(:list) { Fabricate(:list, account: user.account) } diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index 7e350da7e..fe715ff62 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::RelationshipsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb index dbc4b9f3e..8ff2b17de 100644 --- a/spec/controllers/api/v1/accounts/search_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb index 09bb46937..693cd1ac6 100644 --- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Accounts::StatusesController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 7a9e0f8e4..3e54e88a5 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -3,21 +3,38 @@ require 'rails_helper' RSpec.describe Api::V1::AccountsController, type: :controller do render_views - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow read') } + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { '' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before do allow(controller).to receive(:doorkeeper_token) { token } end + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + describe 'GET #show' do - it 'returns http success' do + let(:scopes) { 'read:accounts' } + + before do get :show, params: { id: user.account.id } + end + + it 'returns http success' do expect(response).to have_http_status(200) end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' end describe 'POST #follow' do + let(:scopes) { 'write:follows' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account } before do @@ -41,6 +58,8 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'creates a following relation between user and target user' do expect(user.account.following?(other_account)).to be true end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end context 'with locked account' do @@ -60,10 +79,13 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'creates a follow request relation between user and target user' do expect(user.account.requested?(other_account)).to be true end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end end describe 'POST #unfollow' do + let(:scopes) { 'write:follows' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -78,9 +100,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'removes the following relation between user and target user' do expect(user.account.following?(other_account)).to be false end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end describe 'POST #block' do + let(:scopes) { 'write:blocks' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -99,9 +124,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'creates a blocking relation' do expect(user.account.blocking?(other_account)).to be true end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end describe 'POST #unblock' do + let(:scopes) { 'write:blocks' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -116,9 +144,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'removes the blocking relation between user and target user' do expect(user.account.blocking?(other_account)).to be false end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end describe 'POST #mute' do + let(:scopes) { 'write:mutes' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -141,9 +172,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'mutes notifications' do expect(user.account.muting_notifications?(other_account)).to be true end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end describe 'POST #mute with notifications set to false' do + let(:scopes) { 'write:mutes' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -166,9 +200,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'does not mute notifications' do expect(user.account.muting_notifications?(other_account)).to be false end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end describe 'POST #unmute' do + let(:scopes) { 'write:mutes' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do @@ -183,5 +220,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'removes the muting relation between user and target user' do expect(user.account.muting?(other_account)).to be false end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' end end diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb index eff5fb9da..818f76c92 100644 --- a/spec/controllers/api/v1/blocks_controller_spec.rb +++ b/spec/controllers/api/v1/blocks_controller_spec.rb @@ -3,8 +3,9 @@ require 'rails_helper' RSpec.describe Api::V1::BlocksController, type: :controller do render_views - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'read:blocks' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before { allow(controller).to receive(:doorkeeper_token) { token } } @@ -49,5 +50,14 @@ RSpec.describe Api::V1::BlocksController, type: :controller do get :index expect(response).to have_http_status(200) end + + context 'with wrong scopes' do + let(:scopes) { 'write:blocks' } + + it 'returns http forbidden' do + get :index + expect(response).to have_http_status(403) + end + end end end diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb index bae4612a2..6a7a35c7a 100644 --- a/spec/controllers/api/v1/domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb @@ -4,14 +4,24 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before do user.account.block_domain!('example.com') allow(controller).to receive(:doorkeeper_token) { token } end + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + describe 'GET #show' do + let(:scopes) { 'read:blocks' } + before do get :show, params: { limit: 1 } end @@ -23,9 +33,13 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do it 'returns blocked domains' do expect(body_as_json.first).to eq 'example.com' end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' end describe 'POST #create' do + let(:scopes) { 'write:blocks' } + before do post :create, params: { domain: 'example.org' } end @@ -37,9 +51,13 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do it 'creates a domain block' do expect(user.account.domain_blocking?('example.org')).to be true end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' end describe 'DELETE #destroy' do + let(:scopes) { 'write:blocks' } + before do delete :destroy, params: { domain: 'example.com' } end @@ -51,5 +69,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do it 'deletes a domain block' do expect(user.account.domain_blocking?('example.com')).to be false end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' end end diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb index 46cf70f4d..2bdf927f2 100644 --- a/spec/controllers/api/v1/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/favourites_controller_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do context 'with read scope and valid resource owner' do before do allow(controller).to receive(:doorkeeper_token) do - Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') + Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:favourites') end end diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filter_controller_spec.rb index 3ffd8f784..5948809e3 100644 --- a/spec/controllers/api/v1/filter_controller_spec.rb +++ b/spec/controllers/api/v1/filter_controller_spec.rb @@ -4,13 +4,14 @@ RSpec.describe Api::V1::FiltersController, type: :controller do render_views let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #index' do + let(:scopes) { 'read:filters' } let!(:filter) { Fabricate(:custom_filter, account: user.account) } it 'returns http success' do @@ -20,6 +21,8 @@ RSpec.describe Api::V1::FiltersController, type: :controller do end describe 'POST #create' do + let(:scopes) { 'write:filters' } + before do post :create, params: { phrase: 'magic', context: %w(home), irreversible: true } end @@ -39,6 +42,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do end describe 'GET #show' do + let(:scopes) { 'read:filters' } let(:filter) { Fabricate(:custom_filter, account: user.account) } it 'returns http success' do @@ -48,6 +52,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do end describe 'PUT #update' do + let(:scopes) { 'write:filters' } let(:filter) { Fabricate(:custom_filter, account: user.account) } before do @@ -64,6 +69,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do end describe 'DELETE #destroy' do + let(:scopes) { 'write:filters' } let(:filter) { Fabricate(:custom_filter, account: user.account) } before do diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb index 3c0b84af8..87292d9ce 100644 --- a/spec/controllers/api/v1/follow_requests_controller_spec.rb +++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let(:follower) { Fabricate(:account, username: 'bob') } before do @@ -13,6 +13,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do end describe 'GET #index' do + let(:scopes) { 'read:follows' } + before do get :index, params: { limit: 1 } end @@ -23,6 +25,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do end describe 'POST #authorize' do + let(:scopes) { 'write:follows' } + before do post :authorize, params: { id: follower.id } end @@ -37,6 +41,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do end describe 'POST #reject' do + let(:scopes) { 'write:follows' } + before do post :reject, params: { id: follower.id } end diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb index 38badb80a..089e0fe5e 100644 --- a/spec/controllers/api/v1/follows_controller_spec.rb +++ b/spec/controllers/api/v1/follows_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb index c37a481d6..08c22de56 100644 --- a/spec/controllers/api/v1/lists/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/lists/accounts_controller_spec.rb @@ -4,7 +4,7 @@ describe Api::V1::Lists::AccountsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let(:list) { Fabricate(:list, account: user.account) } before do @@ -14,6 +14,8 @@ describe Api::V1::Lists::AccountsController do end describe 'GET #index' do + let(:scopes) { 'read:lists' } + it 'returns http success' do get :show, params: { list_id: list.id } @@ -22,6 +24,7 @@ describe Api::V1::Lists::AccountsController do end describe 'POST #create' do + let(:scopes) { 'write:lists' } let(:bob) { Fabricate(:account, username: 'bob') } before do @@ -39,6 +42,8 @@ describe Api::V1::Lists::AccountsController do end describe 'DELETE #destroy' do + let(:scopes) { 'write:lists' } + before do delete :destroy, params: { list_id: list.id, account_ids: [list.accounts.first.id] } end diff --git a/spec/controllers/api/v1/lists_controller_spec.rb b/spec/controllers/api/v1/lists_controller_spec.rb index 213429581..e92213789 100644 --- a/spec/controllers/api/v1/lists_controller_spec.rb +++ b/spec/controllers/api/v1/lists_controller_spec.rb @@ -4,12 +4,14 @@ RSpec.describe Api::V1::ListsController, type: :controller do render_views let!(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let!(:list) { Fabricate(:list, account: user.account) } before { allow(controller).to receive(:doorkeeper_token) { token } } describe 'GET #index' do + let(:scopes) { 'read:lists' } + it 'returns http success' do get :index expect(response).to have_http_status(200) @@ -17,6 +19,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do end describe 'GET #show' do + let(:scopes) { 'read:lists' } + it 'returns http success' do get :show, params: { id: list.id } expect(response).to have_http_status(200) @@ -24,6 +28,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do end describe 'POST #create' do + let(:scopes) { 'write:lists' } + before do post :create, params: { title: 'Foo bar' } end @@ -39,6 +45,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do end describe 'PUT #update' do + let(:scopes) { 'write:lists' } + before do put :update, params: { id: list.id, title: 'Updated title' } end @@ -53,6 +61,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do end describe 'DELETE #destroy' do + let(:scopes) { 'write:lists' } + before do delete :destroy, params: { id: list.id } end diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb index ce260eb90..f01fcd942 100644 --- a/spec/controllers/api/v1/media_controller_spec.rb +++ b/spec/controllers/api/v1/media_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:media') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb index dc4a9753a..f9603b7ff 100644 --- a/spec/controllers/api/v1/mutes_controller_spec.rb +++ b/spec/controllers/api/v1/mutes_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:mutes') } before do Fabricate(:mute, account: user.account, hide_notifications: false) diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index 2e6163fcd..9f679cb8a 100644 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/notifications_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } before do @@ -12,6 +12,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do end describe 'GET #show' do + let(:scopes) { 'read:notifications' } + it 'returns http success' do notification = Fabricate(:notification, account: user.account) get :show, params: { id: notification.id } @@ -21,6 +23,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do end describe 'POST #dismiss' do + let(:scopes) { 'write:notifications' } + it 'destroys the notification' do notification = Fabricate(:notification, account: user.account) post :dismiss, params: { id: notification.id } @@ -31,6 +35,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do end describe 'POST #clear' do + let(:scopes) { 'write:notifications' } + it 'clears notifications for the account' do notification = Fabricate(:notification, account: user.account) post :clear @@ -41,6 +47,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do end describe 'GET #index' do + let(:scopes) { 'read:notifications' } + before do first_status = PostStatusService.new.call(user.account, 'Test') @reblog_of_first_status = ReblogService.new.call(other.account, first_status) diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index 1e1ef9308..ac93998c6 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -6,13 +6,15 @@ RSpec.describe Api::V1::ReportsController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #index' do + let(:scopes) { 'read:reports' } + it 'returns http success' do get :index @@ -21,6 +23,7 @@ RSpec.describe Api::V1::ReportsController, type: :controller do end describe 'POST #create' do + let(:scopes) { 'write:reports' } let!(:status) { Fabricate(:status) } let!(:admin) { Fabricate(:user, admin: true) } diff --git a/spec/controllers/api/v1/search_controller_spec.rb b/spec/controllers/api/v1/search_controller_spec.rb index 024703867..c9e544cc7 100644 --- a/spec/controllers/api/v1/search_controller_spec.rb +++ b/spec/controllers/api/v1/search_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Api::V1::SearchController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb index c873e05dd..23b5d7de9 100644 --- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb index 53f602616..24a760e20 100644 --- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb @@ -7,7 +7,7 @@ describe Api::V1::Statuses::FavouritesController do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:favourites', application: app) } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb index 13b4625d1..966398580 100644 --- a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb @@ -7,7 +7,7 @@ describe Api::V1::Statuses::MutesController do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:mutes', application: app) } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb index 8f5b0800b..13405d285 100644 --- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb @@ -7,7 +7,7 @@ describe Api::V1::Statuses::PinsController do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:accounts', application: app) } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb index 9c0c2b60c..d758786dc 100644 --- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb index e60f8da2a..d14ca3e8b 100644 --- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb @@ -7,7 +7,7 @@ describe Api::V1::Statuses::ReblogsController do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:statuses', application: app) } context 'with an oauth token' do before do diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 27e4f4eb2..8bc3b0c67 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: scopes) } context 'with an oauth token' do before do @@ -13,6 +13,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do end describe 'GET #show' do + let(:scopes) { 'read:statuses' } let(:status) { Fabricate(:status, account: user.account) } it 'returns http success' do @@ -22,6 +23,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do end describe 'GET #context' do + let(:scopes) { 'read:statuses' } let(:status) { Fabricate(:status, account: user.account) } before do @@ -35,6 +37,8 @@ RSpec.describe Api::V1::StatusesController, type: :controller do end describe 'POST #create' do + let(:scopes) { 'write:statuses' } + before do post :create, params: { status: 'Hello world' } end @@ -45,6 +49,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do end describe 'DELETE #destroy' do + let(:scopes) { 'write:statuses' } let(:status) { Fabricate(:status, account: user.account) } before do diff --git a/spec/controllers/api/v1/timelines/home_controller_spec.rb b/spec/controllers/api/v1/timelines/home_controller_spec.rb index 85b031641..a667c33fa 100644 --- a/spec/controllers/api/v1/timelines/home_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/home_controller_spec.rb @@ -12,7 +12,7 @@ describe Api::V1::Timelines::HomeController do end context 'with a user context' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } describe 'GET #show' do before do diff --git a/spec/controllers/api/v1/timelines/list_controller_spec.rb b/spec/controllers/api/v1/timelines/list_controller_spec.rb index 1729217c9..93a2be6e6 100644 --- a/spec/controllers/api/v1/timelines/list_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/list_controller_spec.rb @@ -13,7 +13,7 @@ describe Api::V1::Timelines::ListController do end context 'with a user context' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') } describe 'GET #show' do before do diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb new file mode 100644 index 000000000..8ee8753de --- /dev/null +++ b/spec/controllers/api/v2/search_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V2::SearchController, type: :controller do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { q: 'test' } + + expect(response).to have_http_status(200) + end + end +end -- cgit From cd509d2146439490bcb63081739db1d5fd485d56 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 7 Jul 2018 18:51:56 +0200 Subject: Remove .p-name microformat class (#7961) Fix #7926 --- app/views/stream_entries/_detailed_status.html.haml | 2 +- app/views/stream_entries/_simple_status.html.haml | 2 +- spec/views/stream_entries/show.html.haml_spec.rb | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index de844818f..3fc715c4e 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -12,7 +12,7 @@ = render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg') = t('accounts.follow') - .status__content.p-name.emojify< + .status__content.emojify< - if status.spoiler_text? %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{Formatter.instance.format_spoiler(status)}  diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index b89860ad9..0f27585a1 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -13,7 +13,7 @@ %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) - .status__content.p-name.emojify< + .status__content.emojify< - if status.spoiler_text? %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{Formatter.instance.format_spoiler(status)}  diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb index 560039ffa..e0681b125 100644 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ b/spec/views/stream_entries/show.html.haml_spec.rb @@ -30,9 +30,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d mf2 = Microformats.parse(rendered) - expect(mf2.entry.name.to_s).to eq status.text expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.author.name.to_s).to eq alice.display_name expect(mf2.entry.author.url.to_s).not_to be_empty end @@ -56,9 +54,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d mf2 = Microformats.parse(rendered) - expect(mf2.entry.name.to_s).to eq reply.text expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.comment.url.to_s).not_to be_empty expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name expect(mf2.entry.comment.author.url.to_s).not_to be_empty -- cgit From 1ca4e51eb38de6de81cedf3ddcdaa626f1d1c569 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 9 Jul 2018 02:22:09 +0200 Subject: Add option to not consider word boundaries when processing keyword filtering (#7975) * Add option to not consider word boundaries when filtering phrases * Add a few tests for keyword/phrase filtering --- app/controllers/api/v1/filters_controller.rb | 2 +- app/javascript/mastodon/selectors/index.js | 5 +++- app/lib/feed_manager.rb | 11 ++++++++- app/models/custom_filter.rb | 1 + app/serializers/rest/filter_serializer.rb | 2 +- app/views/filters/_fields.html.haml | 3 +++ ...180707154237_add_whole_word_to_custom_filter.rb | 17 +++++++++++++ db/schema.rb | 3 ++- spec/lib/feed_manager_spec.rb | 28 +++++++++++++++++----- 9 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20180707154237_add_whole_word_to_custom_filter.rb (limited to 'spec') diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index a98080d1d..e5ebaff4d 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -43,6 +43,6 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, context: []) + params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) end end diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 7aa7569a0..d0212c379 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -45,7 +45,10 @@ export const regexFromFilters = filters => { return null; } - return new RegExp(filters.map(filter => escapeRegExp(filter.get('phrase'))).map(expr => `\\b${expr}\\b`).join('|'), 'i'); + return new RegExp(filters.map(filter => { + let expr = escapeRegExp(filter.get('phrase')); + return filter.get('whole_word') ? `\\b${expr}\\b` : expr; + }).join('|'), 'i'); }; export const makeGetStatus = () => { diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 55c72d0ea..c247ab21d 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -200,7 +200,16 @@ class FeedManager active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a active_filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? } - active_filters.map! { |filter| Regexp.new("\\b#{Regexp.escape(filter.phrase)}\\b", true) } + active_filters.map! do |filter| + if filter.whole_word + sb = filter.phrase =~ /\A[[:word:]]/ ? '\b' : '' + eb = filter.phrase =~ /[[:word:]]\Z/ ? '\b' : '' + + /(?mix:#{sb}#{Regexp.escape(filter.phrase)}#{eb})/ + else + /#{Regexp.escape(filter.phrase)}/i + end + end return false if active_filters.empty? diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 2c1a54375..342207a55 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -8,6 +8,7 @@ # expires_at :datetime # phrase :text default(""), not null # context :string default([]), not null, is an Array +# whole_word :boolean default(TRUE), not null # irreversible :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/serializers/rest/filter_serializer.rb b/app/serializers/rest/filter_serializer.rb index 51340aa79..3134be371 100644 --- a/app/serializers/rest/filter_serializer.rb +++ b/app/serializers/rest/filter_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class REST::FilterSerializer < ActiveModel::Serializer - attributes :id, :phrase, :context, :expires_at, + attributes :id, :phrase, :context, :whole_word, :expires_at, :irreversible end diff --git a/app/views/filters/_fields.html.haml b/app/views/filters/_fields.html.haml index af5d648b8..a5a3f0337 100644 --- a/app/views/filters/_fields.html.haml +++ b/app/views/filters/_fields.html.haml @@ -7,5 +7,8 @@ .fields-group = f.input :irreversible, wrapper: :with_label +.fields-group + = f.input :whole_word, wrapper: :with_label + .fields-group = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') diff --git a/db/migrate/20180707154237_add_whole_word_to_custom_filter.rb b/db/migrate/20180707154237_add_whole_word_to_custom_filter.rb new file mode 100644 index 000000000..63ecb8741 --- /dev/null +++ b/db/migrate/20180707154237_add_whole_word_to_custom_filter.rb @@ -0,0 +1,17 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddWholeWordToCustomFilter < ActiveRecord::Migration[5.2] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def change + safety_assured do + add_column_with_default :custom_filters, :whole_word, :boolean, default: true, allow_null: false + end + end + + def down + remove_column :custom_filters, :whole_word + end +end diff --git a/db/schema.rb b/db/schema.rb index 661fc8179..02032c548 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: 2018_06_28_181026) do +ActiveRecord::Schema.define(version: 2018_07_07_154237) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -149,6 +149,7 @@ ActiveRecord::Schema.define(version: 2018_06_28_181026) do t.text "phrase", default: "", null: false t.string "context", default: [], null: false, array: true t.boolean "irreversible", default: false, null: false + t.boolean "whole_word", default: true, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["account_id"], name: "index_custom_filters_on_account_id" diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index d1b847675..7535e144d 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -127,12 +127,28 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end - it 'returns true if status contains irreversibly muted phrase' do - alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true) - alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true) - alice.follow!(jeff) - status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff) - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + context 'for irreversibly muted phrases' do + it 'considers word boundaries when matching' do + alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true) + alice.follow!(jeff) + status = Fabricate(:status, text: 'bobcats', account: jeff) + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy + end + + it 'returns true if phrase is contained' do + alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true) + alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true) + alice.follow!(jeff) + status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff) + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end + + it 'matches substrings if whole_word is false' do + alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true) + alice.follow!(jeff) + status = Fabricate(:status, text: 'shiitake', account: jeff) + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end end end -- cgit From 402da8065c2b378cca6361f2c7252bd766f25dde Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 7 Jul 2018 21:40:49 +0200 Subject: Migrate glitch-soc keyword mutes to Mastodon's Completely remove glitch-soc's Keyword Mutes, migrate existing database records to CustomFilters. Handling of client-side filters is still not implemented in the glitch-soc front-end. --- .../settings/keyword_mutes_controller.rb | 61 -------- app/lib/feed_manager.rb | 22 --- app/models/glitch.rb | 7 - app/models/glitch/keyword_mute.rb | 123 --------------- app/models/glitch/keyword_mute_helper.rb | 27 ---- config/navigation.rb | 1 - config/routes.rb | 6 - db/migrate/20180707193142_migrate_filters.rb | 54 +++++++ db/schema.rb | 13 +- .../settings/keyword_mutes_controller_spec.rb | 5 - spec/fabricators/glitch_keyword_mute_fabricator.rb | 2 - spec/lib/feed_manager_spec.rb | 76 --------- spec/models/glitch/keyword_mute_helper_spec.rb | 52 ------- spec/models/glitch/keyword_mute_spec.rb | 171 --------------------- 14 files changed, 55 insertions(+), 565 deletions(-) delete mode 100644 app/controllers/settings/keyword_mutes_controller.rb delete mode 100644 app/models/glitch.rb delete mode 100644 app/models/glitch/keyword_mute.rb delete mode 100644 app/models/glitch/keyword_mute_helper.rb create mode 100644 db/migrate/20180707193142_migrate_filters.rb delete mode 100644 spec/controllers/settings/keyword_mutes_controller_spec.rb delete mode 100644 spec/fabricators/glitch_keyword_mute_fabricator.rb delete mode 100644 spec/models/glitch/keyword_mute_helper_spec.rb delete mode 100644 spec/models/glitch/keyword_mute_spec.rb (limited to 'spec') diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb deleted file mode 100644 index cf364a903..000000000 --- a/app/controllers/settings/keyword_mutes_controller.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -class Settings::KeywordMutesController < Settings::BaseController - before_action :load_keyword_mute, only: [:edit, :update, :destroy] - - def index - @keyword_mutes = paginated_keyword_mutes_for_account - end - - def new - @keyword_mute = keyword_mutes_for_account.build - end - - def create - @keyword_mute = keyword_mutes_for_account.create(keyword_mute_params) - - if @keyword_mute.persisted? - redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg') - else - render :new - end - end - - def update - if @keyword_mute.update(keyword_mute_params) - redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg') - else - render :edit - end - end - - def destroy - @keyword_mute.destroy! - - redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg') - end - - def destroy_all - keyword_mutes_for_account.delete_all - - redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg') - end - - private - - def keyword_mutes_for_account - Glitch::KeywordMute.where(account: current_account) - end - - def load_keyword_mute - @keyword_mute = keyword_mutes_for_account.find(params[:id]) - end - - def keyword_mute_params - params.require(:keyword_mute).permit(:keyword, :whole_word, :apply_to_mentions) - end - - def paginated_keyword_mutes_for_account - keyword_mutes_for_account.order(:keyword).page params[:page] - end -end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index dff3cbb22..c247ab21d 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -153,7 +153,6 @@ class FeedManager def filter_from_home?(status, receiver_id) return false if receiver_id == status.account_id return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) - return true if keyword_filter_from_home?(status, receiver_id) return true if phrase_filtered?(status, receiver_id, :home) check_for_blocks = status.mentions.pluck(:account_id) @@ -181,26 +180,6 @@ class FeedManager false end - def keyword_filter_from_home?(status, receiver_id) - # If this status mentions the receiver, use the mentions scope: it's - # possible that the status will show up in the receiver's mentions, which - # means it ought to show up in the home feed as well. - # - # If it doesn't mention the receiver but is still headed for the home feed, - # use the home feed scope. - scope = if status.mentions.pluck(:account_id).include?(receiver_id) - Glitch::KeywordMute::Scopes::Mentions - else - Glitch::KeywordMute::Scopes::HomeFeed - end - - return true if keyword_filter?(status, receiver_id, scope) - end - - def keyword_filter?(status, receiver_id, scope) - Glitch::KeywordMuteHelper.new(receiver_id).matches?(status, scope) - end - def filter_from_mentions?(status, receiver_id) return true if receiver_id == status.account_id return true if phrase_filtered?(status, receiver_id, :notifications) @@ -213,7 +192,6 @@ class FeedManager should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them - should_filter ||= keyword_filter?(status, receiver_id, Glitch::KeywordMute::Scopes::Mentions) # or if the mention contains a muted keyword should_filter end diff --git a/app/models/glitch.rb b/app/models/glitch.rb deleted file mode 100644 index 0e497babc..000000000 --- a/app/models/glitch.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Glitch - def self.table_name_prefix - 'glitch_' - end -end diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb deleted file mode 100644 index 49769cb73..000000000 --- a/app/models/glitch/keyword_mute.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: glitch_keyword_mutes -# -# id :bigint(8) not null, primary key -# account_id :bigint(8) not null -# keyword :string not null -# whole_word :boolean default(TRUE), not null -# created_at :datetime not null -# updated_at :datetime not null -# apply_to_mentions :boolean default(TRUE), not null -# - -class Glitch::KeywordMute < ApplicationRecord - belongs_to :account, required: true - - validates_presence_of :keyword - - after_commit :invalidate_cached_matchers - - module Scopes - Unscoped = 0b00 - HomeFeed = 0b01 - Mentions = 0b10 - end - - def self.text_matcher_for(account_id) - TextMatcher.new(account_id) - end - - def self.tag_matcher_for(account_id) - TagMatcher.new(account_id) - end - - def scope - s = Scopes::Unscoped - s |= Scopes::HomeFeed - s |= Scopes::Mentions if apply_to_mentions? - s - end - - private - - def invalidate_cached_matchers - Rails.cache.delete(TextMatcher.cache_key(account_id)) - Rails.cache.delete(TagMatcher.cache_key(account_id)) - end - - class CachedKeywordMute - attr_reader :keyword - attr_reader :whole_word - attr_reader :scope - - def initialize(keyword, whole_word, scope) - @keyword = keyword - @whole_word = whole_word - @scope = scope - end - - def boundary_regex_for_keyword - sb = keyword =~ /\A[[:word:]]/ ? '\b' : '' - eb = keyword =~ /[[:word:]]\Z/ ? '\b' : '' - - /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/ - end - - def matches?(str, required_scope) - ((required_scope & scope) == required_scope) && \ - str =~ (whole_word ? boundary_regex_for_keyword : /#{Regexp.escape(keyword)}/i) - end - end - - class Matcher - attr_reader :account_id - attr_reader :keywords - - def initialize(account_id) - @account_id = account_id - @keywords = Rails.cache.fetch(self.class.cache_key(account_id)) { fetch_keywords } - end - - protected - - def fetch_keywords - Glitch::KeywordMute.select(:whole_word, :keyword, :apply_to_mentions) - .where(account_id: account_id) - .map { |kw| CachedKeywordMute.new(transform_keyword(kw.keyword), kw.whole_word, kw.scope) } - end - - def transform_keyword(keyword) - keyword - end - end - - class TextMatcher < Matcher - def self.cache_key(account_id) - format('keyword_mutes:regex:text:%s', account_id) - end - - def matches?(str, scope) - keywords.any? { |kw| kw.matches?(str, scope) } - end - end - - class TagMatcher < Matcher - def self.cache_key(account_id) - format('keyword_mutes:regex:tag:%s', account_id) - end - - def matches?(tags, scope) - tags.pluck(:name).any? do |n| - keywords.any? { |kw| kw.matches?(n, scope) } - end - end - - protected - - def transform_keyword(kw) - Tag::HASHTAG_RE =~ kw ? $1 : kw - end - end -end diff --git a/app/models/glitch/keyword_mute_helper.rb b/app/models/glitch/keyword_mute_helper.rb deleted file mode 100644 index 955c3b1f3..000000000 --- a/app/models/glitch/keyword_mute_helper.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'html2text' - -class Glitch::KeywordMuteHelper - attr_reader :text_matcher - attr_reader :tag_matcher - - def initialize(receiver_id) - @text_matcher = Glitch::KeywordMute.text_matcher_for(receiver_id) - @tag_matcher = Glitch::KeywordMute.tag_matcher_for(receiver_id) - end - - def matches?(status, scope) - matchers_match?(status, scope) || (status.reblog? && matchers_match?(status.reblog, scope)) - end - - private - - def matchers_match?(status, scope) - text_matcher.matches?(prepare_text(status.text), scope) || - text_matcher.matches?(prepare_text(status.spoiler_text), scope) || - tag_matcher.matches?(status.tags, scope) - end - - def prepare_text(text) - Html2Text.convert(text) - end -end diff --git a/config/navigation.rb b/config/navigation.rb index 6cb13612c..460027595 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -7,7 +7,6 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url - settings.item :keyword_mutes, safe_join([fa_icon('volume-off fw'), t('settings.keyword_mutes')]), settings_keyword_mutes_url settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication} diff --git a/config/routes.rb b/config/routes.rb index 326cd1edf..9778d8b41 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,12 +77,6 @@ Rails.application.routes.draw do namespace :settings do resource :profile, only: [:show, :update] - resources :keyword_mutes do - collection do - delete :destroy_all - end - end - resource :preferences, only: [:show, :update] resource :notifications, only: [:show, :update] resource :import, only: [:show, :create] diff --git a/db/migrate/20180707193142_migrate_filters.rb b/db/migrate/20180707193142_migrate_filters.rb new file mode 100644 index 000000000..455ce71ed --- /dev/null +++ b/db/migrate/20180707193142_migrate_filters.rb @@ -0,0 +1,54 @@ +class MigrateFilters < ActiveRecord::Migration[5.2] + class GlitchKeywordMute < ApplicationRecord + # Dummy class, as we removed Glitch::KeywordMute + belongs_to :account, required: true + validates_presence_of :keyword + end + + class CustomFilter < ApplicationRecord + # Dummy class, in case CustomFilter gets altered in the future + belongs_to :account + validates :phrase, :context, presence: true + + before_validation :clean_up_contexts + + private + + def clean_up_contexts + self.context = Array(context).map(&:strip).map(&:presence).compact + end + end + + disable_ddl_transaction! + + def up + GlitchKeywordMute.find_each do |filter| + filter.account.custom_filters.create!( + phrase: filter.keyword, + context: filter.apply_to_mentions ? %w(home public notifications) : %w(home public), + whole_word: filter.whole_word, + irreversible: true) + end + + drop_table :glitch_keyword_mutes + end + + def down + create_table "glitch_keyword_mutes" do |t| + t.references :account, null: false + t.string :keyword, null: false + t.boolean :whole_word, default: true, null: false + t.boolean :apply_to_mentions, default: true, null: false + t.timestamps + end + + add_foreign_key :glitch_keyword_mutes, :accounts, on_delete: :cascade + + CustomFilter.where(irreversible: true).find_each do |filter| + GlitchKeywordMute.where(account: filter.account).create!( + keyword: filter.phrase, + whole_word: filter.whole_word, + apply_to_mentions: filter.context.include?('notifications')) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 602bb6755..cf42e80e9 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: 2018_07_07_154237) do +ActiveRecord::Schema.define(version: 2018_07_07_193142) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -211,16 +211,6 @@ ActiveRecord::Schema.define(version: 2018_07_07_154237) do t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end - create_table "glitch_keyword_mutes", force: :cascade do |t| - t.bigint "account_id", null: false - t.string "keyword", null: false - t.boolean "whole_word", default: true, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "apply_to_mentions", default: true, null: false - t.index ["account_id"], name: "index_glitch_keyword_mutes_on_account_id" - end - create_table "identities", id: :serial, force: :cascade do |t| t.integer "user_id" t.string "provider", default: "", null: false @@ -603,7 +593,6 @@ ActiveRecord::Schema.define(version: 2018_07_07_154237) do add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade - add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade add_foreign_key "identities", "users", on_delete: :cascade add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade add_foreign_key "invites", "users", on_delete: :cascade diff --git a/spec/controllers/settings/keyword_mutes_controller_spec.rb b/spec/controllers/settings/keyword_mutes_controller_spec.rb deleted file mode 100644 index a8c37a072..000000000 --- a/spec/controllers/settings/keyword_mutes_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe Settings::KeywordMutesController, type: :controller do - -end diff --git a/spec/fabricators/glitch_keyword_mute_fabricator.rb b/spec/fabricators/glitch_keyword_mute_fabricator.rb deleted file mode 100644 index 20d393320..000000000 --- a/spec/fabricators/glitch_keyword_mute_fabricator.rb +++ /dev/null @@ -1,2 +0,0 @@ -Fabricator('Glitch::KeywordMute') do -end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 8305530c5..a36ebad49 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -134,68 +134,6 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end - it 'returns true for a status containing a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - status = Fabricate(:status, text: 'This is a hot take', account: bob) - - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true - end - - it 'returns true for a reply containing a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - s1 = Fabricate(:status, text: 'Something', account: alice) - s2 = Fabricate(:status, text: 'This is a hot take', thread: s1, account: bob) - - expect(FeedManager.instance.filter?(:home, s2, alice.id)).to be true - end - - it 'returns true for a status whose spoiler text contains a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob) - - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true - end - - it 'returns true for a reblog containing a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - status = Fabricate(:status, text: 'This is a hot take', account: bob) - reblog = Fabricate(:status, reblog: status, account: jeff) - - expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true - end - - it 'returns true for a reblog whose spoiler text contains a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob) - reblog = Fabricate(:status, reblog: status, account: jeff) - - expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true - end - - it 'returns true for a status with a tag that matches a muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'jorts') - status = Fabricate(:status, account: bob) - status.tags << Fabricate(:tag, name: 'jorts') - - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true - end - - it 'returns true for a status with a tag that matches an octothorpe-prefixed muted keyword' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: '#jorts') - status = Fabricate(:status, account: bob) - status.tags << Fabricate(:tag, name: 'jorts') - - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true - end - - it 'returns false if the status is muted by a keyword mute that does not apply to mentions' do - Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take', apply_to_mentions: false) - status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob) - status.mentions.create!(account_id: alice.id) - - expect(FeedManager.instance.filter?(:home, status, alice.id)).to be false - end - context 'for irreversibly muted phrases' do it 'considers word boundaries when matching' do alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true) @@ -247,20 +185,6 @@ RSpec.describe FeedManager do bob.follow!(alice) expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false end - - it 'returns true for status that contains a muted keyword' do - Fabricate('Glitch::KeywordMute', account: bob, keyword: 'take') - status = Fabricate(:status, text: 'This is a hot take', account: alice) - bob.follow!(alice) - expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true - end - - it 'returns false for a mention that contains a word muted by a keyword that does not apply to mentions' do - Fabricate('Glitch::KeywordMute', account: bob, keyword: 'take', apply_to_mentions: false) - status = Fabricate(:status, text: 'This is a hot take', account: alice) - bob.follow!(alice) - expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false - end end end diff --git a/spec/models/glitch/keyword_mute_helper_spec.rb b/spec/models/glitch/keyword_mute_helper_spec.rb deleted file mode 100644 index f9408d62d..000000000 --- a/spec/models/glitch/keyword_mute_helper_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -RSpec.describe Glitch::KeywordMuteHelper do - describe '#matches?' do - Unscoped = Glitch::KeywordMute::Scopes::Unscoped - - let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } - let(:helper) { Glitch::KeywordMuteHelper.new(alice) } - - it 'ignores names of HTML tags in status text' do - status = Fabricate(:status, text: 'uh example') - Glitch::KeywordMute.create!(account: alice, keyword: 'addr') - - expect(helper.matches?(status, Unscoped)).to be false - end - - it 'ignores properties of HTML tags in status text' do - status = Fabricate(:status, text: 'uh example') - Glitch::KeywordMute.create!(account: alice, keyword: 'href') - - expect(helper.matches?(status, Unscoped)).to be false - end - - it 'matches text inside HTML tags' do - status = Fabricate(:status, text: '

HEY THIS IS SOMETHING ANNOYING

') - Glitch::KeywordMute.create!(account: alice, keyword: 'annoying') - - expect(helper.matches?(status, Unscoped)).to be true - end - - it 'matches < in HTML-stripped text' do - status = Fabricate(:status, text: '

I <3 oats

') - Glitch::KeywordMute.create!(account: alice, keyword: '<3') - - expect(helper.matches?(status, Unscoped)).to be true - end - - it 'matches < in HTML text' do - status = Fabricate(:status, text: '

I <3 oats

') - Glitch::KeywordMute.create!(account: alice, keyword: '<3') - - expect(helper.matches?(status, Unscoped)).to be true - end - - it 'matches link hrefs in HTML text' do - status = Fabricate(:status, text: '

yep

') - Glitch::KeywordMute.create!(account: alice, keyword: 'milk') - - expect(helper.matches?(status, Unscoped)).to be true - end - end -end diff --git a/spec/models/glitch/keyword_mute_spec.rb b/spec/models/glitch/keyword_mute_spec.rb deleted file mode 100644 index 12c354738..000000000 --- a/spec/models/glitch/keyword_mute_spec.rb +++ /dev/null @@ -1,171 +0,0 @@ -require 'rails_helper' - -RSpec.describe Glitch::KeywordMute, type: :model do - let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } - let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) } - - Unscoped = Glitch::KeywordMute::Scopes::Unscoped - - describe '.text_matcher_for' do - let(:matcher) { Glitch::KeywordMute.text_matcher_for(alice.id) } - - describe 'with no mutes' do - before do - Glitch::KeywordMute.delete_all - end - - it 'does not match' do - expect(matcher.matches?('This is a hot take', Unscoped)).to be_falsy - end - end - - describe 'with mutes' do - it 'does not match keywords set by a different account' do - Glitch::KeywordMute.create!(account: bob, keyword: 'take') - - expect(matcher.matches?('This is a hot take', Unscoped)).to be_falsy - end - - it 'does not match if no keywords match the status text' do - Glitch::KeywordMute.create!(account: alice, keyword: 'cold') - - expect(matcher.matches?('This is a hot take', Unscoped)).to be_falsy - end - - it 'considers word boundaries when matching' do - Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true) - - expect(matcher.matches?('bobcats', Unscoped)).to be_falsy - end - - it 'matches substrings if whole_word is false' do - Glitch::KeywordMute.create!(account: alice, keyword: 'take', whole_word: false) - - expect(matcher.matches?('This is a shiitake mushroom', Unscoped)).to be_truthy - end - - it 'matches keywords at the beginning of the text' do - Glitch::KeywordMute.create!(account: alice, keyword: 'take') - - expect(matcher.matches?('Take this', Unscoped)).to be_truthy - end - - it 'matches keywords at the end of the text' do - Glitch::KeywordMute.create!(account: alice, keyword: 'take') - - expect(matcher.matches?('This is a hot take', Unscoped)).to be_truthy - end - - it 'matches if at least one keyword case-insensitively matches the text' do - Glitch::KeywordMute.create!(account: alice, keyword: 'hot') - - expect(matcher.matches?('This is a HOT take', Unscoped)).to be_truthy - end - - it 'matches if at least one non-whole-word keyword case-insensitively matches the text' do - Glitch::KeywordMute.create!(account: alice, keyword: 'hot', whole_word: false) - - expect(matcher.matches?('This is a HOTTY take', Unscoped)).to be_truthy - end - - it 'maintains case-insensitivity when combining keywords into a single matcher' do - Glitch::KeywordMute.create!(account: alice, keyword: 'hot') - Glitch::KeywordMute.create!(account: alice, keyword: 'cold') - - expect(matcher.matches?('This is a HOT take', Unscoped)).to be_truthy - end - - it 'matches keywords surrounded by non-alphanumeric ornamentation' do - Glitch::KeywordMute.create!(account: alice, keyword: 'hot') - - expect(matcher.matches?('(hot take)', Unscoped)).to be_truthy - end - - it 'escapes metacharacters in whole-word keywords' do - Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)') - - expect(matcher.matches?('(hot take)', Unscoped)).to be_truthy - end - - it 'escapes metacharacters in non-whole-word keywords' do - Glitch::KeywordMute.create!(account: alice, keyword: '(-', whole_word: false) - - expect(matcher.matches?('bad (-)', Unscoped)).to be_truthy - end - - it 'uses case-folding rules appropriate for more than just English' do - Glitch::KeywordMute.create!(account: alice, keyword: 'großeltern') - - expect(matcher.matches?('besuch der grosseltern', Unscoped)).to be_truthy - end - - it 'matches keywords that are composed of multiple words' do - Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake') - - expect(matcher.matches?('This is a shiitake', Unscoped)).to be_truthy - expect(matcher.matches?('This is shiitake', Unscoped)).to_not be_truthy - end - end - end - - describe '.tag_matcher_for' do - let(:matcher) { Glitch::KeywordMute.tag_matcher_for(alice.id) } - let(:status) { Fabricate(:status) } - - describe 'with no mutes' do - before do - Glitch::KeywordMute.delete_all - end - - it 'does not match' do - status.tags << Fabricate(:tag, name: 'xyzzy') - - expect(matcher.matches?(status.tags, Unscoped)).to be false - end - end - - describe 'with mutes' do - it 'does not match keywords set by a different account' do - status.tags << Fabricate(:tag, name: 'xyzzy') - Glitch::KeywordMute.create!(account: bob, keyword: 'take') - - expect(matcher.matches?(status.tags, Unscoped)).to be false - end - - it 'matches #xyzzy when given the mute "#xyzzy"' do - status.tags << Fabricate(:tag, name: 'xyzzy') - Glitch::KeywordMute.create!(account: alice, keyword: '#xyzzy') - - expect(matcher.matches?(status.tags, Unscoped)).to be true - end - - it 'matches #thingiverse when given the non-whole-word mute "#thing"' do - status.tags << Fabricate(:tag, name: 'thingiverse') - Glitch::KeywordMute.create!(account: alice, keyword: '#thing', whole_word: false) - - expect(matcher.matches?(status.tags, Unscoped)).to be true - end - - it 'matches #hashtag when given the mute "##hashtag""' do - status.tags << Fabricate(:tag, name: 'hashtag') - Glitch::KeywordMute.create!(account: alice, keyword: '##hashtag') - - expect(matcher.matches?(status.tags, Unscoped)).to be true - end - - it 'matches #oatmeal when given the non-whole-word mute "oat"' do - status.tags << Fabricate(:tag, name: 'oatmeal') - Glitch::KeywordMute.create!(account: alice, keyword: 'oat', whole_word: false) - - expect(matcher.matches?(status.tags, Unscoped)).to be true - end - - it 'does not match #oatmeal when given the mute "#oat"' do - status.tags << Fabricate(:tag, name: 'oatmeal') - Glitch::KeywordMute.create!(account: alice, keyword: 'oat') - - expect(matcher.matches?(status.tags, Unscoped)).to be false - end - end - end -end -- cgit