Keyword mutingclosed-social-glitch-2
@ -0,0 +1,64 @@ | |||
# frozen_string_literal: true | |||
class Settings::KeywordMutesController < ApplicationController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
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) | |||
end | |||
def paginated_keyword_mutes_for_account | |||
keyword_mutes_for_account.order(:keyword).page params[:page] | |||
end | |||
end |
@ -0,0 +1,2 @@ | |||
module Settings::KeywordMutesHelper | |||
end |
@ -0,0 +1,7 @@ | |||
# frozen_string_literal: true | |||
module Glitch | |||
def self.table_name_prefix | |||
'glitch_' | |||
end | |||
end |
@ -0,0 +1,66 @@ | |||
# frozen_string_literal: true | |||
# == Schema Information | |||
# | |||
# Table name: glitch_keyword_mutes | |||
# | |||
# id :integer not null, primary key | |||
# account_id :integer not null | |||
# keyword :string not null | |||
# whole_word :boolean default(TRUE), not null | |||
# created_at :datetime not null | |||
# updated_at :datetime not null | |||
# | |||
class Glitch::KeywordMute < ApplicationRecord | |||
belongs_to :account, required: true | |||
validates_presence_of :keyword | |||
after_commit :invalidate_cached_matcher | |||
def self.matcher_for(account_id) | |||
Matcher.new(account_id) | |||
end | |||
private | |||
def invalidate_cached_matcher | |||
Rails.cache.delete("keyword_mutes:regex:#{account_id}") | |||
end | |||
class Matcher | |||
attr_reader :account_id | |||
attr_reader :regex | |||
def initialize(account_id) | |||
@account_id = account_id | |||
regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account } | |||
@regex = /#{regex_text}/i | |||
end | |||
def =~(str) | |||
regex =~ str | |||
end | |||
private | |||
def keywords | |||
Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word) | |||
end | |||
def regex_text_for_account | |||
kws = keywords.find_each.with_object([]) do |kw, a| | |||
a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword) | |||
end | |||
Regexp.union(kws).source | |||
end | |||
def boundary_regex_for_keyword(keyword) | |||
sb = keyword =~ /\A[[:word:]]/ ? '\b' : '' | |||
eb = keyword =~ /[[:word:]]\Z/ ? '\b' : '' | |||
/#{sb}#{Regexp.escape(keyword)}#{eb}/ | |||
end | |||
end | |||
end |
@ -0,0 +1,11 @@ | |||
.fields-group | |||
= f.input :keyword | |||
= f.check_box :whole_word | |||
= f.label :whole_word, t('keyword_mutes.match_whole_word') | |||
.actions | |||
- if f.object.persisted? | |||
= f.button :button, t('generic.save_changes'), type: :submit | |||
= link_to t('keyword_mutes.remove'), settings_keyword_mute_path(f.object), class: 'negative button', method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } | |||
- else | |||
= f.button :button, t('keyword_mutes.add_keyword'), type: :submit |
@ -0,0 +1,10 @@ | |||
%tr | |||
%td | |||
= keyword_mute.keyword | |||
%td | |||
- if keyword_mute.whole_word | |||
%i.fa.fa-check | |||
%td | |||
= table_link_to 'edit', t('keyword_mutes.edit'), edit_settings_keyword_mute_path(keyword_mute) | |||
%td | |||
= table_link_to 'times', t('keyword_mutes.remove'), settings_keyword_mute_path(keyword_mute), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } |
@ -0,0 +1,6 @@ | |||
- content_for :page_title do | |||
= t('keyword_mutes.edit_keyword') | |||
= simple_form_for @keyword_mute, url: settings_keyword_mute_path(@keyword_mute), as: :keyword_mute do |f| | |||
= render 'shared/error_messages', object: @keyword_mute | |||
= render 'fields', f: f |
@ -0,0 +1,18 @@ | |||
- content_for :page_title do | |||
= t('settings.keyword_mutes') | |||
.table-wrapper | |||
%table.table | |||
%thead | |||
%tr | |||
%th= t('keyword_mutes.keyword') | |||
%th= t('keyword_mutes.match_whole_word') | |||
%th | |||
%th | |||
%tbody | |||
= render partial: 'keyword_mute', collection: @keyword_mutes, as: :keyword_mute | |||
= paginate @keyword_mutes | |||
.simple_form | |||
= link_to t('keyword_mutes.add_keyword'), new_settings_keyword_mute_path, class: 'button' | |||
= link_to t('keyword_mutes.remove_all'), destroy_all_settings_keyword_mutes_path, class: 'button negative', method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } |
@ -0,0 +1,6 @@ | |||
- content_for :page_title do | |||
= t('keyword_mutes.add_keyword') | |||
= simple_form_for @keyword_mute, url: settings_keyword_mutes_path, as: :keyword_mute do |f| | |||
= render 'shared/error_messages', object: @keyword_mute | |||
= render 'fields', f: f |
@ -0,0 +1,12 @@ | |||
class CreateKeywordMutes < ActiveRecord::Migration[5.1] | |||
def change | |||
create_table :keyword_mutes do |t| | |||
t.references :account, null: false | |||
t.string :keyword, null: false | |||
t.boolean :whole_word, null: false, default: true | |||
t.timestamps | |||
end | |||
add_foreign_key :keyword_mutes, :accounts, on_delete: :cascade | |||
end | |||
end |
@ -0,0 +1,7 @@ | |||
class MoveKeywordMutesIntoGlitchNamespace < ActiveRecord::Migration[5.1] | |||
def change | |||
safety_assured do | |||
rename_table :keyword_mutes, :glitch_keyword_mutes | |||
end | |||
end | |||
end |
@ -0,0 +1,5 @@ | |||
require 'rails_helper' | |||
RSpec.describe Settings::KeywordMutesController, type: :controller do | |||
end |
@ -0,0 +1,2 @@ | |||
Fabricator('Glitch::KeywordMute') do | |||
end |
@ -0,0 +1,15 @@ | |||
require 'rails_helper' | |||
# Specs in this file have access to a helper object that includes | |||
# the Settings::KeywordMutesHelper. For example: | |||
# | |||
# describe Settings::KeywordMutesHelper do | |||
# describe "string concat" do | |||
# it "concats two strings with spaces" do | |||
# expect(helper.concat_strings("this","that")).to eq("this that") | |||
# end | |||
# end | |||
# end | |||
RSpec.describe Settings::KeywordMutesHelper, type: :helper do | |||
pending "add some examples to (or delete) #{__FILE__}" | |||
end |
@ -0,0 +1,89 @@ | |||
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!) } | |||
describe '.matcher_for' do | |||
let(:matcher) { Glitch::KeywordMute.matcher_for(alice) } | |||
describe 'with no mutes' do | |||
before do | |||
Glitch::KeywordMute.delete_all | |||
end | |||
it 'does not match' do | |||
expect(matcher =~ 'This is a hot take').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 =~ 'This is a hot take').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 =~ 'This is a hot take').to be_falsy | |||
end | |||
it 'considers word boundaries when matching' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true) | |||
expect(matcher =~ 'bobcats').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 =~ 'This is a shiitake mushroom').to be_truthy | |||
end | |||
it 'matches keywords at the beginning of the text' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: 'take') | |||
expect(matcher =~ 'Take this').to be_truthy | |||
end | |||
it 'matches keywords at the end of the text' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: 'take') | |||
expect(matcher =~ 'This is a hot take').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 =~ 'This is a HOT take').to be_truthy | |||
end | |||
it 'matches keywords surrounded by non-alphanumeric ornamentation' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: 'hot') | |||
expect(matcher =~ '(hot take)').to be_truthy | |||
end | |||
it 'escapes metacharacters in keywords' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)') | |||
expect(matcher =~ '(hot take)').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 =~ 'besuch der grosseltern').to be_truthy | |||
end | |||
it 'matches keywords that are composed of multiple words' do | |||
Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake') | |||
expect(matcher =~ 'This is a shiitake').to be_truthy | |||
expect(matcher =~ 'This is shiitake').to_not be_truthy | |||
end | |||
end | |||
end | |||
end |